##// END OF EJS Templates
Assignable users should not include users that cannot view the tracker (#23172)....
Jean-Philippe Lang -
r15204:83777f727a42
parent child
Show More
@@ -1,97 +1,89
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ContextMenusController < ApplicationController
19 19 helper :watchers
20 20 helper :issues
21 21
22 22 before_filter :find_issues, :only => :issues
23 23
24 24 def issues
25 25 if (@issues.size == 1)
26 26 @issue = @issues.first
27 27 end
28 28 @issue_ids = @issues.map(&:id).sort
29 29
30 30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
31 31
32 32 @can = {:edit => @issues.all?(&:attributes_editable?),
33 33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
34 34 :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?,
35 35 :add_watchers => User.current.allowed_to?(:add_issue_watchers, @projects),
36 36 :delete => @issues.all?(&:deletable?)
37 37 }
38 if @project
39 if @issue
40 @assignables = @issue.assignable_users
41 else
42 @assignables = @project.assignable_users
43 end
44 else
45 #when multiple projects, we only keep the intersection of each set
46 @assignables = @projects.map(&:assignable_users).reduce(:&)
47 end
38
39 @assignables = @issues.map(&:assignable_users).reduce(:&)
48 40 @trackers = @projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
49 41 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
50 42
51 43 @priorities = IssuePriority.active.reverse
52 44 @back = back_url
53 45
54 46 @options_by_custom_field = {}
55 47 if @can[:edit]
56 48 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
57 49 custom_fields.each do |field|
58 50 values = field.possible_values_options(@projects)
59 51 if values.present?
60 52 @options_by_custom_field[field] = values
61 53 end
62 54 end
63 55 end
64 56
65 57 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
66 58 render :layout => false
67 59 end
68 60
69 61 def time_entries
70 62 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
71 63 (render_404; return) unless @time_entries.present?
72 64 if (@time_entries.size == 1)
73 65 @time_entry = @time_entries.first
74 66 end
75 67
76 68 @projects = @time_entries.collect(&:project).compact.uniq
77 69 @project = @projects.first if @projects.size == 1
78 70 @activities = TimeEntryActivity.shared.active
79 71
80 72 edit_allowed = @time_entries.all? {|t| t.editable_by?(User.current)}
81 73 @can = {:edit => edit_allowed, :delete => edit_allowed}
82 74 @back = back_url
83 75
84 76 @options_by_custom_field = {}
85 77 if @can[:edit]
86 78 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
87 79 custom_fields.each do |field|
88 80 values = field.possible_values_options(@projects)
89 81 if values.present?
90 82 @options_by_custom_field[field] = values
91 83 end
92 84 end
93 85 end
94 86
95 87 render :layout => false
96 88 end
97 89 end
@@ -1,1708 +1,1708
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::Utils::DateCalculation
21 21 include Redmine::I18n
22 22 before_save :set_parent_id
23 23 include Redmine::NestedSet::IssueNestedSet
24 24
25 25 belongs_to :project
26 26 belongs_to :tracker
27 27 belongs_to :status, :class_name => 'IssueStatus'
28 28 belongs_to :author, :class_name => 'User'
29 29 belongs_to :assigned_to, :class_name => 'Principal'
30 30 belongs_to :fixed_version, :class_name => 'Version'
31 31 belongs_to :priority, :class_name => 'IssuePriority'
32 32 belongs_to :category, :class_name => 'IssueCategory'
33 33
34 34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 35 has_many :visible_journals,
36 36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 37 :class_name => 'Journal',
38 38 :as => :journalized
39 39
40 40 has_many :time_entries, :dependent => :destroy
41 41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 47 acts_as_customizable
48 48 acts_as_watchable
49 49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 50 :preload => [:project, :status, :tracker],
51 51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52 52
53 53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 56
57 57 acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
58 58 :author_key => :author_id
59 59
60 60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 61
62 62 attr_reader :current_journal
63 63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 64
65 65 validates_presence_of :subject, :project, :tracker
66 66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69 69
70 70 validates_length_of :subject, :maximum => 255
71 71 validates_inclusion_of :done_ratio, :in => 0..100
72 72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 73 validates :start_date, :date => true
74 74 validates :due_date, :date => true
75 75 validate :validate_issue, :validate_required_fields
76 76 attr_protected :id
77 77
78 78 scope :visible, lambda {|*args|
79 79 joins(:project).
80 80 where(Issue.visible_condition(args.shift || User.current, *args))
81 81 }
82 82
83 83 scope :open, lambda {|*args|
84 84 is_closed = args.size > 0 ? !args.first : false
85 85 joins(:status).
86 86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 87 }
88 88
89 89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 90 scope :on_active_project, lambda {
91 91 joins(:project).
92 92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 93 }
94 94 scope :fixed_version, lambda {|versions|
95 95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 97 }
98 98 scope :assigned_to, lambda {|arg|
99 99 arg = Array(arg).uniq
100 100 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
101 101 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
102 102 ids.compact!
103 103 ids.any? ? where(:assigned_to_id => ids) : none
104 104 }
105 105
106 106 before_validation :clear_disabled_fields
107 107 before_create :default_assign
108 108 before_save :close_duplicates, :update_done_ratio_from_issue_status,
109 109 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
110 110 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
111 111 after_save :reschedule_following_issues, :update_nested_set_attributes,
112 112 :update_parent_attributes, :create_journal
113 113 # Should be after_create but would be called before previous after_save callbacks
114 114 after_save :after_create_from_copy
115 115 after_destroy :update_parent_attributes
116 116 after_create :send_notification
117 117 # Keep it at the end of after_save callbacks
118 118 after_save :clear_assigned_to_was
119 119
120 120 # Returns a SQL conditions string used to find all issues visible by the specified user
121 121 def self.visible_condition(user, options={})
122 122 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
123 123 sql = if user.id && user.logged?
124 124 case role.issues_visibility
125 125 when 'all'
126 126 '1=1'
127 127 when 'default'
128 128 user_ids = [user.id] + user.groups.map(&:id).compact
129 129 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
130 130 when 'own'
131 131 user_ids = [user.id] + user.groups.map(&:id).compact
132 132 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
133 133 else
134 134 '1=0'
135 135 end
136 136 else
137 137 "(#{table_name}.is_private = #{connection.quoted_false})"
138 138 end
139 139 unless role.permissions_all_trackers?(:view_issues)
140 140 tracker_ids = role.permissions_tracker_ids(:view_issues)
141 141 if tracker_ids.any?
142 142 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
143 143 else
144 144 sql = '1=0'
145 145 end
146 146 end
147 147 sql
148 148 end
149 149 end
150 150
151 151 # Returns true if usr or current user is allowed to view the issue
152 152 def visible?(usr=nil)
153 153 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
154 154 visible = if user.logged?
155 155 case role.issues_visibility
156 156 when 'all'
157 157 true
158 158 when 'default'
159 159 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
160 160 when 'own'
161 161 self.author == user || user.is_or_belongs_to?(assigned_to)
162 162 else
163 163 false
164 164 end
165 165 else
166 166 !self.is_private?
167 167 end
168 168 unless role.permissions_all_trackers?(:view_issues)
169 169 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
170 170 end
171 171 visible
172 172 end
173 173 end
174 174
175 175 # Returns true if user or current user is allowed to edit or add notes to the issue
176 176 def editable?(user=User.current)
177 177 attributes_editable?(user) || notes_addable?(user)
178 178 end
179 179
180 180 # Returns true if user or current user is allowed to edit the issue
181 181 def attributes_editable?(user=User.current)
182 182 user_tracker_permission?(user, :edit_issues)
183 183 end
184 184
185 185 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
186 186 def attachments_editable?(user=User.current)
187 187 attributes_editable?(user)
188 188 end
189 189
190 190 # Returns true if user or current user is allowed to add notes to the issue
191 191 def notes_addable?(user=User.current)
192 192 user_tracker_permission?(user, :add_issue_notes)
193 193 end
194 194
195 195 # Returns true if user or current user is allowed to delete the issue
196 196 def deletable?(user=User.current)
197 197 user_tracker_permission?(user, :delete_issues)
198 198 end
199 199
200 200 def initialize(attributes=nil, *args)
201 201 super
202 202 if new_record?
203 203 # set default values for new records only
204 204 self.priority ||= IssuePriority.default
205 205 self.watcher_user_ids = []
206 206 end
207 207 end
208 208
209 209 def create_or_update
210 210 super
211 211 ensure
212 212 @status_was = nil
213 213 end
214 214 private :create_or_update
215 215
216 216 # AR#Persistence#destroy would raise and RecordNotFound exception
217 217 # if the issue was already deleted or updated (non matching lock_version).
218 218 # This is a problem when bulk deleting issues or deleting a project
219 219 # (because an issue may already be deleted if its parent was deleted
220 220 # first).
221 221 # The issue is reloaded by the nested_set before being deleted so
222 222 # the lock_version condition should not be an issue but we handle it.
223 223 def destroy
224 224 super
225 225 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
226 226 # Stale or already deleted
227 227 begin
228 228 reload
229 229 rescue ActiveRecord::RecordNotFound
230 230 # The issue was actually already deleted
231 231 @destroyed = true
232 232 return freeze
233 233 end
234 234 # The issue was stale, retry to destroy
235 235 super
236 236 end
237 237
238 238 alias :base_reload :reload
239 239 def reload(*args)
240 240 @workflow_rule_by_attribute = nil
241 241 @assignable_versions = nil
242 242 @relations = nil
243 243 @spent_hours = nil
244 244 @total_spent_hours = nil
245 245 @total_estimated_hours = nil
246 246 base_reload(*args)
247 247 end
248 248
249 249 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
250 250 def available_custom_fields
251 251 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
252 252 end
253 253
254 254 def visible_custom_field_values(user=nil)
255 255 user_real = user || User.current
256 256 custom_field_values.select do |value|
257 257 value.custom_field.visible_by?(project, user_real)
258 258 end
259 259 end
260 260
261 261 # Copies attributes from another issue, arg can be an id or an Issue
262 262 def copy_from(arg, options={})
263 263 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
264 264 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
265 265 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
266 266 self.status = issue.status
267 267 self.author = User.current
268 268 unless options[:attachments] == false
269 269 self.attachments = issue.attachments.map do |attachement|
270 270 attachement.copy(:container => self)
271 271 end
272 272 end
273 273 @copied_from = issue
274 274 @copy_options = options
275 275 self
276 276 end
277 277
278 278 # Returns an unsaved copy of the issue
279 279 def copy(attributes=nil, copy_options={})
280 280 copy = self.class.new.copy_from(self, copy_options)
281 281 copy.attributes = attributes if attributes
282 282 copy
283 283 end
284 284
285 285 # Returns true if the issue is a copy
286 286 def copy?
287 287 @copied_from.present?
288 288 end
289 289
290 290 def status_id=(status_id)
291 291 if status_id.to_s != self.status_id.to_s
292 292 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
293 293 end
294 294 self.status_id
295 295 end
296 296
297 297 # Sets the status.
298 298 def status=(status)
299 299 if status != self.status
300 300 @workflow_rule_by_attribute = nil
301 301 end
302 302 association(:status).writer(status)
303 303 end
304 304
305 305 def priority_id=(pid)
306 306 self.priority = nil
307 307 write_attribute(:priority_id, pid)
308 308 end
309 309
310 310 def category_id=(cid)
311 311 self.category = nil
312 312 write_attribute(:category_id, cid)
313 313 end
314 314
315 315 def fixed_version_id=(vid)
316 316 self.fixed_version = nil
317 317 write_attribute(:fixed_version_id, vid)
318 318 end
319 319
320 320 def tracker_id=(tracker_id)
321 321 if tracker_id.to_s != self.tracker_id.to_s
322 322 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
323 323 end
324 324 self.tracker_id
325 325 end
326 326
327 327 # Sets the tracker.
328 328 # This will set the status to the default status of the new tracker if:
329 329 # * the status was the default for the previous tracker
330 330 # * or if the status was not part of the new tracker statuses
331 331 # * or the status was nil
332 332 def tracker=(tracker)
333 333 tracker_was = self.tracker
334 334 association(:tracker).writer(tracker)
335 335 if tracker != tracker_was
336 336 if status == tracker_was.try(:default_status)
337 337 self.status = nil
338 338 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
339 339 self.status = nil
340 340 end
341 341 reassign_custom_field_values
342 342 @workflow_rule_by_attribute = nil
343 343 end
344 344 self.status ||= default_status
345 345 self.tracker
346 346 end
347 347
348 348 def project_id=(project_id)
349 349 if project_id.to_s != self.project_id.to_s
350 350 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
351 351 end
352 352 self.project_id
353 353 end
354 354
355 355 # Sets the project.
356 356 # Unless keep_tracker argument is set to true, this will change the tracker
357 357 # to the first tracker of the new project if the previous tracker is not part
358 358 # of the new project trackers.
359 359 # This will:
360 360 # * clear the fixed_version is it's no longer valid for the new project.
361 361 # * clear the parent issue if it's no longer valid for the new project.
362 362 # * set the category to the category with the same name in the new
363 363 # project if it exists, or clear it if it doesn't.
364 364 # * for new issue, set the fixed_version to the project default version
365 365 # if it's a valid fixed_version.
366 366 def project=(project, keep_tracker=false)
367 367 project_was = self.project
368 368 association(:project).writer(project)
369 369 if project_was && project && project_was != project
370 370 @assignable_versions = nil
371 371
372 372 unless keep_tracker || project.trackers.include?(tracker)
373 373 self.tracker = project.trackers.first
374 374 end
375 375 # Reassign to the category with same name if any
376 376 if category
377 377 self.category = project.issue_categories.find_by_name(category.name)
378 378 end
379 379 # Keep the fixed_version if it's still valid in the new_project
380 380 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
381 381 self.fixed_version = nil
382 382 end
383 383 # Clear the parent task if it's no longer valid
384 384 unless valid_parent_project?
385 385 self.parent_issue_id = nil
386 386 end
387 387 reassign_custom_field_values
388 388 @workflow_rule_by_attribute = nil
389 389 end
390 390 # Set fixed_version to the project default version if it's valid
391 391 if new_record? && fixed_version.nil? && project && project.default_version_id?
392 392 if project.shared_versions.open.exists?(project.default_version_id)
393 393 self.fixed_version_id = project.default_version_id
394 394 end
395 395 end
396 396 self.project
397 397 end
398 398
399 399 def description=(arg)
400 400 if arg.is_a?(String)
401 401 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
402 402 end
403 403 write_attribute(:description, arg)
404 404 end
405 405
406 406 # Overrides assign_attributes so that project and tracker get assigned first
407 407 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
408 408 return if new_attributes.nil?
409 409 attrs = new_attributes.dup
410 410 attrs.stringify_keys!
411 411
412 412 %w(project project_id tracker tracker_id).each do |attr|
413 413 if attrs.has_key?(attr)
414 414 send "#{attr}=", attrs.delete(attr)
415 415 end
416 416 end
417 417 send :assign_attributes_without_project_and_tracker_first, attrs, *args
418 418 end
419 419 # Do not redefine alias chain on reload (see #4838)
420 420 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
421 421
422 422 def attributes=(new_attributes)
423 423 assign_attributes new_attributes
424 424 end
425 425
426 426 def estimated_hours=(h)
427 427 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
428 428 end
429 429
430 430 safe_attributes 'project_id',
431 431 'tracker_id',
432 432 'status_id',
433 433 'category_id',
434 434 'assigned_to_id',
435 435 'priority_id',
436 436 'fixed_version_id',
437 437 'subject',
438 438 'description',
439 439 'start_date',
440 440 'due_date',
441 441 'done_ratio',
442 442 'estimated_hours',
443 443 'custom_field_values',
444 444 'custom_fields',
445 445 'lock_version',
446 446 'notes',
447 447 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
448 448
449 449 safe_attributes 'notes',
450 450 :if => lambda {|issue, user| issue.notes_addable?(user)}
451 451
452 452 safe_attributes 'private_notes',
453 453 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
454 454
455 455 safe_attributes 'watcher_user_ids',
456 456 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
457 457
458 458 safe_attributes 'is_private',
459 459 :if => lambda {|issue, user|
460 460 user.allowed_to?(:set_issues_private, issue.project) ||
461 461 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
462 462 }
463 463
464 464 safe_attributes 'parent_issue_id',
465 465 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
466 466 user.allowed_to?(:manage_subtasks, issue.project)}
467 467
468 468 def safe_attribute_names(user=nil)
469 469 names = super
470 470 names -= disabled_core_fields
471 471 names -= read_only_attribute_names(user)
472 472 if new_record?
473 473 # Make sure that project_id can always be set for new issues
474 474 names |= %w(project_id)
475 475 end
476 476 if dates_derived?
477 477 names -= %w(start_date due_date)
478 478 end
479 479 if priority_derived?
480 480 names -= %w(priority_id)
481 481 end
482 482 if done_ratio_derived?
483 483 names -= %w(done_ratio)
484 484 end
485 485 names
486 486 end
487 487
488 488 # Safely sets attributes
489 489 # Should be called from controllers instead of #attributes=
490 490 # attr_accessible is too rough because we still want things like
491 491 # Issue.new(:project => foo) to work
492 492 def safe_attributes=(attrs, user=User.current)
493 493 return unless attrs.is_a?(Hash)
494 494
495 495 attrs = attrs.deep_dup
496 496
497 497 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
498 498 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
499 499 if allowed_target_projects(user).where(:id => p.to_i).exists?
500 500 self.project_id = p
501 501 end
502 502
503 503 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
504 504 # Discard submitted category on previous project
505 505 attrs.delete('category_id')
506 506 end
507 507 end
508 508
509 509 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
510 510 if allowed_target_trackers(user).where(:id => t.to_i).exists?
511 511 self.tracker_id = t
512 512 end
513 513 end
514 514 if project
515 515 # Set a default tracker to accept custom field values
516 516 # even if tracker is not specified
517 517 self.tracker ||= allowed_target_trackers(user).first
518 518 end
519 519
520 520 statuses_allowed = new_statuses_allowed_to(user)
521 521 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
522 522 if statuses_allowed.collect(&:id).include?(s.to_i)
523 523 self.status_id = s
524 524 end
525 525 end
526 526 if new_record? && !statuses_allowed.include?(status)
527 527 self.status = statuses_allowed.first || default_status
528 528 end
529 529 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
530 530 if u.blank?
531 531 self.assigned_to_id = nil
532 532 else
533 533 u = u.to_i
534 534 if assignable_users.any?{|assignable_user| assignable_user.id == u}
535 535 self.assigned_to_id = u
536 536 end
537 537 end
538 538 end
539 539
540 540
541 541 attrs = delete_unsafe_attributes(attrs, user)
542 542 return if attrs.empty?
543 543
544 544 if attrs['parent_issue_id'].present?
545 545 s = attrs['parent_issue_id'].to_s
546 546 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
547 547 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
548 548 end
549 549 end
550 550
551 551 if attrs['custom_field_values'].present?
552 552 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
553 553 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
554 554 end
555 555
556 556 if attrs['custom_fields'].present?
557 557 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
558 558 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
559 559 end
560 560
561 561 # mass-assignment security bypass
562 562 assign_attributes attrs, :without_protection => true
563 563 end
564 564
565 565 def disabled_core_fields
566 566 tracker ? tracker.disabled_core_fields : []
567 567 end
568 568
569 569 # Returns the custom_field_values that can be edited by the given user
570 570 def editable_custom_field_values(user=nil)
571 571 visible_custom_field_values(user).reject do |value|
572 572 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
573 573 end
574 574 end
575 575
576 576 # Returns the custom fields that can be edited by the given user
577 577 def editable_custom_fields(user=nil)
578 578 editable_custom_field_values(user).map(&:custom_field).uniq
579 579 end
580 580
581 581 # Returns the names of attributes that are read-only for user or the current user
582 582 # For users with multiple roles, the read-only fields are the intersection of
583 583 # read-only fields of each role
584 584 # The result is an array of strings where sustom fields are represented with their ids
585 585 #
586 586 # Examples:
587 587 # issue.read_only_attribute_names # => ['due_date', '2']
588 588 # issue.read_only_attribute_names(user) # => []
589 589 def read_only_attribute_names(user=nil)
590 590 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
591 591 end
592 592
593 593 # Returns the names of required attributes for user or the current user
594 594 # For users with multiple roles, the required fields are the intersection of
595 595 # required fields of each role
596 596 # The result is an array of strings where sustom fields are represented with their ids
597 597 #
598 598 # Examples:
599 599 # issue.required_attribute_names # => ['due_date', '2']
600 600 # issue.required_attribute_names(user) # => []
601 601 def required_attribute_names(user=nil)
602 602 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
603 603 end
604 604
605 605 # Returns true if the attribute is required for user
606 606 def required_attribute?(name, user=nil)
607 607 required_attribute_names(user).include?(name.to_s)
608 608 end
609 609
610 610 # Returns a hash of the workflow rule by attribute for the given user
611 611 #
612 612 # Examples:
613 613 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
614 614 def workflow_rule_by_attribute(user=nil)
615 615 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
616 616
617 617 user_real = user || User.current
618 618 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
619 619 roles = roles.select(&:consider_workflow?)
620 620 return {} if roles.empty?
621 621
622 622 result = {}
623 623 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
624 624 if workflow_permissions.any?
625 625 workflow_rules = workflow_permissions.inject({}) do |h, wp|
626 626 h[wp.field_name] ||= {}
627 627 h[wp.field_name][wp.role_id] = wp.rule
628 628 h
629 629 end
630 630 fields_with_roles = {}
631 631 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
632 632 fields_with_roles[field_id] ||= []
633 633 fields_with_roles[field_id] << role_id
634 634 end
635 635 roles.each do |role|
636 636 fields_with_roles.each do |field_id, role_ids|
637 637 unless role_ids.include?(role.id)
638 638 field_name = field_id.to_s
639 639 workflow_rules[field_name] ||= {}
640 640 workflow_rules[field_name][role.id] = 'readonly'
641 641 end
642 642 end
643 643 end
644 644 workflow_rules.each do |attr, rules|
645 645 next if rules.size < roles.size
646 646 uniq_rules = rules.values.uniq
647 647 if uniq_rules.size == 1
648 648 result[attr] = uniq_rules.first
649 649 else
650 650 result[attr] = 'required'
651 651 end
652 652 end
653 653 end
654 654 @workflow_rule_by_attribute = result if user.nil?
655 655 result
656 656 end
657 657 private :workflow_rule_by_attribute
658 658
659 659 def done_ratio
660 660 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
661 661 status.default_done_ratio
662 662 else
663 663 read_attribute(:done_ratio)
664 664 end
665 665 end
666 666
667 667 def self.use_status_for_done_ratio?
668 668 Setting.issue_done_ratio == 'issue_status'
669 669 end
670 670
671 671 def self.use_field_for_done_ratio?
672 672 Setting.issue_done_ratio == 'issue_field'
673 673 end
674 674
675 675 def validate_issue
676 676 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
677 677 errors.add :due_date, :greater_than_start_date
678 678 end
679 679
680 680 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
681 681 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
682 682 end
683 683
684 684 if fixed_version
685 685 if !assignable_versions.include?(fixed_version)
686 686 errors.add :fixed_version_id, :inclusion
687 687 elsif reopening? && fixed_version.closed?
688 688 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
689 689 end
690 690 end
691 691
692 692 # Checks that the issue can not be added/moved to a disabled tracker
693 693 if project && (tracker_id_changed? || project_id_changed?)
694 694 if tracker && !project.trackers.include?(tracker)
695 695 errors.add :tracker_id, :inclusion
696 696 end
697 697 end
698 698
699 699 # Checks parent issue assignment
700 700 if @invalid_parent_issue_id.present?
701 701 errors.add :parent_issue_id, :invalid
702 702 elsif @parent_issue
703 703 if !valid_parent_project?(@parent_issue)
704 704 errors.add :parent_issue_id, :invalid
705 705 elsif (@parent_issue != parent) && (
706 706 self.would_reschedule?(@parent_issue) ||
707 707 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
708 708 )
709 709 errors.add :parent_issue_id, :invalid
710 710 elsif !new_record?
711 711 # moving an existing issue
712 712 if move_possible?(@parent_issue)
713 713 # move accepted
714 714 else
715 715 errors.add :parent_issue_id, :invalid
716 716 end
717 717 end
718 718 end
719 719 end
720 720
721 721 # Validates the issue against additional workflow requirements
722 722 def validate_required_fields
723 723 user = new_record? ? author : current_journal.try(:user)
724 724
725 725 required_attribute_names(user).each do |attribute|
726 726 if attribute =~ /^\d+$/
727 727 attribute = attribute.to_i
728 728 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
729 729 if v && Array(v.value).detect(&:present?).nil?
730 730 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
731 731 end
732 732 else
733 733 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
734 734 next if attribute == 'category_id' && project.try(:issue_categories).blank?
735 735 next if attribute == 'fixed_version_id' && assignable_versions.blank?
736 736 errors.add attribute, :blank
737 737 end
738 738 end
739 739 end
740 740 end
741 741
742 742 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
743 743 # so that custom values that are not editable are not validated (eg. a custom field that
744 744 # is marked as required should not trigger a validation error if the user is not allowed
745 745 # to edit this field).
746 746 def validate_custom_field_values
747 747 user = new_record? ? author : current_journal.try(:user)
748 748 if new_record? || custom_field_values_changed?
749 749 editable_custom_field_values(user).each(&:validate_value)
750 750 end
751 751 end
752 752
753 753 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
754 754 # even if the user turns off the setting later
755 755 def update_done_ratio_from_issue_status
756 756 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
757 757 self.done_ratio = status.default_done_ratio
758 758 end
759 759 end
760 760
761 761 def init_journal(user, notes = "")
762 762 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
763 763 end
764 764
765 765 # Returns the current journal or nil if it's not initialized
766 766 def current_journal
767 767 @current_journal
768 768 end
769 769
770 770 # Returns the names of attributes that are journalized when updating the issue
771 771 def journalized_attribute_names
772 772 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
773 773 if tracker
774 774 names -= tracker.disabled_core_fields
775 775 end
776 776 names
777 777 end
778 778
779 779 # Returns the id of the last journal or nil
780 780 def last_journal_id
781 781 if new_record?
782 782 nil
783 783 else
784 784 journals.maximum(:id)
785 785 end
786 786 end
787 787
788 788 # Returns a scope for journals that have an id greater than journal_id
789 789 def journals_after(journal_id)
790 790 scope = journals.reorder("#{Journal.table_name}.id ASC")
791 791 if journal_id.present?
792 792 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
793 793 end
794 794 scope
795 795 end
796 796
797 797 # Returns the initial status of the issue
798 798 # Returns nil for a new issue
799 799 def status_was
800 800 if status_id_changed?
801 801 if status_id_was.to_i > 0
802 802 @status_was ||= IssueStatus.find_by_id(status_id_was)
803 803 end
804 804 else
805 805 @status_was ||= status
806 806 end
807 807 end
808 808
809 809 # Return true if the issue is closed, otherwise false
810 810 def closed?
811 811 status.present? && status.is_closed?
812 812 end
813 813
814 814 # Returns true if the issue was closed when loaded
815 815 def was_closed?
816 816 status_was.present? && status_was.is_closed?
817 817 end
818 818
819 819 # Return true if the issue is being reopened
820 820 def reopening?
821 821 if new_record?
822 822 false
823 823 else
824 824 status_id_changed? && !closed? && was_closed?
825 825 end
826 826 end
827 827 alias :reopened? :reopening?
828 828
829 829 # Return true if the issue is being closed
830 830 def closing?
831 831 if new_record?
832 832 closed?
833 833 else
834 834 status_id_changed? && closed? && !was_closed?
835 835 end
836 836 end
837 837
838 838 # Returns true if the issue is overdue
839 839 def overdue?
840 840 due_date.present? && (due_date < User.current.today) && !closed?
841 841 end
842 842
843 843 # Is the amount of work done less than it should for the due date
844 844 def behind_schedule?
845 845 return false if start_date.nil? || due_date.nil?
846 846 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
847 847 return done_date <= User.current.today
848 848 end
849 849
850 850 # Does this issue have children?
851 851 def children?
852 852 !leaf?
853 853 end
854 854
855 855 # Users the issue can be assigned to
856 856 def assignable_users
857 users = project.assignable_users.to_a
857 users = project.assignable_users(tracker).to_a
858 858 users << author if author && author.active?
859 859 users << assigned_to if assigned_to
860 860 users.uniq.sort
861 861 end
862 862
863 863 # Versions that the issue can be assigned to
864 864 def assignable_versions
865 865 return @assignable_versions if @assignable_versions
866 866
867 867 versions = project.shared_versions.open.to_a
868 868 if fixed_version
869 869 if fixed_version_id_changed?
870 870 # nothing to do
871 871 elsif project_id_changed?
872 872 if project.shared_versions.include?(fixed_version)
873 873 versions << fixed_version
874 874 end
875 875 else
876 876 versions << fixed_version
877 877 end
878 878 end
879 879 @assignable_versions = versions.uniq.sort
880 880 end
881 881
882 882 # Returns true if this issue is blocked by another issue that is still open
883 883 def blocked?
884 884 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
885 885 end
886 886
887 887 # Returns the default status of the issue based on its tracker
888 888 # Returns nil if tracker is nil
889 889 def default_status
890 890 tracker.try(:default_status)
891 891 end
892 892
893 893 # Returns an array of statuses that user is able to apply
894 894 def new_statuses_allowed_to(user=User.current, include_default=false)
895 895 if new_record? && @copied_from
896 896 [default_status, @copied_from.status].compact.uniq.sort
897 897 else
898 898 initial_status = nil
899 899 if new_record?
900 900 # nop
901 901 elsif tracker_id_changed?
902 902 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
903 903 initial_status = default_status
904 904 elsif tracker.issue_status_ids.include?(status_id_was)
905 905 initial_status = IssueStatus.find_by_id(status_id_was)
906 906 else
907 907 initial_status = default_status
908 908 end
909 909 else
910 910 initial_status = status_was
911 911 end
912 912
913 913 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
914 914 assignee_transitions_allowed = initial_assigned_to_id.present? &&
915 915 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
916 916
917 917 statuses = []
918 918 statuses += IssueStatus.new_statuses_allowed(
919 919 initial_status,
920 920 user.admin ? Role.all.to_a : user.roles_for_project(project),
921 921 tracker,
922 922 author == user,
923 923 assignee_transitions_allowed
924 924 )
925 925 statuses << initial_status unless statuses.empty?
926 926 statuses << default_status if include_default || (new_record? && statuses.empty?)
927 927 statuses = statuses.compact.uniq.sort
928 928 if blocked?
929 929 statuses.reject!(&:is_closed?)
930 930 end
931 931 statuses
932 932 end
933 933 end
934 934
935 935 # Returns the previous assignee (user or group) if changed
936 936 def assigned_to_was
937 937 # assigned_to_id_was is reset before after_save callbacks
938 938 user_id = @previous_assigned_to_id || assigned_to_id_was
939 939 if user_id && user_id != assigned_to_id
940 940 @assigned_to_was ||= Principal.find_by_id(user_id)
941 941 end
942 942 end
943 943
944 944 # Returns the original tracker
945 945 def tracker_was
946 946 Tracker.find_by_id(tracker_id_was)
947 947 end
948 948
949 949 # Returns the users that should be notified
950 950 def notified_users
951 951 notified = []
952 952 # Author and assignee are always notified unless they have been
953 953 # locked or don't want to be notified
954 954 notified << author if author
955 955 if assigned_to
956 956 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
957 957 end
958 958 if assigned_to_was
959 959 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
960 960 end
961 961 notified = notified.select {|u| u.active? && u.notify_about?(self)}
962 962
963 963 notified += project.notified_users
964 964 notified.uniq!
965 965 # Remove users that can not view the issue
966 966 notified.reject! {|user| !visible?(user)}
967 967 notified
968 968 end
969 969
970 970 # Returns the email addresses that should be notified
971 971 def recipients
972 972 notified_users.collect(&:mail)
973 973 end
974 974
975 975 def each_notification(users, &block)
976 976 if users.any?
977 977 if custom_field_values.detect {|value| !value.custom_field.visible?}
978 978 users_by_custom_field_visibility = users.group_by do |user|
979 979 visible_custom_field_values(user).map(&:custom_field_id).sort
980 980 end
981 981 users_by_custom_field_visibility.values.each do |users|
982 982 yield(users)
983 983 end
984 984 else
985 985 yield(users)
986 986 end
987 987 end
988 988 end
989 989
990 990 def notify?
991 991 @notify != false
992 992 end
993 993
994 994 def notify=(arg)
995 995 @notify = arg
996 996 end
997 997
998 998 # Returns the number of hours spent on this issue
999 999 def spent_hours
1000 1000 @spent_hours ||= time_entries.sum(:hours) || 0
1001 1001 end
1002 1002
1003 1003 # Returns the total number of hours spent on this issue and its descendants
1004 1004 def total_spent_hours
1005 1005 @total_spent_hours ||= if leaf?
1006 1006 spent_hours
1007 1007 else
1008 1008 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1009 1009 end
1010 1010 end
1011 1011
1012 1012 def total_estimated_hours
1013 1013 if leaf?
1014 1014 estimated_hours
1015 1015 else
1016 1016 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1017 1017 end
1018 1018 end
1019 1019
1020 1020 def relations
1021 1021 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1022 1022 end
1023 1023
1024 1024 # Preloads relations for a collection of issues
1025 1025 def self.load_relations(issues)
1026 1026 if issues.any?
1027 1027 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1028 1028 issues.each do |issue|
1029 1029 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1030 1030 end
1031 1031 end
1032 1032 end
1033 1033
1034 1034 # Preloads visible spent time for a collection of issues
1035 1035 def self.load_visible_spent_hours(issues, user=User.current)
1036 1036 if issues.any?
1037 1037 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1038 1038 issues.each do |issue|
1039 1039 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1040 1040 end
1041 1041 end
1042 1042 end
1043 1043
1044 1044 # Preloads visible total spent time for a collection of issues
1045 1045 def self.load_visible_total_spent_hours(issues, user=User.current)
1046 1046 if issues.any?
1047 1047 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1048 1048 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1049 1049 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1050 1050 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1051 1051 issues.each do |issue|
1052 1052 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1053 1053 end
1054 1054 end
1055 1055 end
1056 1056
1057 1057 # Preloads visible relations for a collection of issues
1058 1058 def self.load_visible_relations(issues, user=User.current)
1059 1059 if issues.any?
1060 1060 issue_ids = issues.map(&:id)
1061 1061 # Relations with issue_from in given issues and visible issue_to
1062 1062 relations_from = IssueRelation.joins(:issue_to => :project).
1063 1063 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1064 1064 # Relations with issue_to in given issues and visible issue_from
1065 1065 relations_to = IssueRelation.joins(:issue_from => :project).
1066 1066 where(visible_condition(user)).
1067 1067 where(:issue_to_id => issue_ids).to_a
1068 1068 issues.each do |issue|
1069 1069 relations =
1070 1070 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1071 1071 relations_to.select {|relation| relation.issue_to_id == issue.id}
1072 1072
1073 1073 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1074 1074 end
1075 1075 end
1076 1076 end
1077 1077
1078 1078 # Finds an issue relation given its id.
1079 1079 def find_relation(relation_id)
1080 1080 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1081 1081 end
1082 1082
1083 1083 # Returns true if this issue blocks the other issue, otherwise returns false
1084 1084 def blocks?(other)
1085 1085 all = [self]
1086 1086 last = [self]
1087 1087 while last.any?
1088 1088 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1089 1089 current -= last
1090 1090 current -= all
1091 1091 return true if current.include?(other)
1092 1092 last = current
1093 1093 all += last
1094 1094 end
1095 1095 false
1096 1096 end
1097 1097
1098 1098 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1099 1099 def would_reschedule?(other)
1100 1100 all = [self]
1101 1101 last = [self]
1102 1102 while last.any?
1103 1103 current = last.map {|i|
1104 1104 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1105 1105 i.leaves.to_a +
1106 1106 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1107 1107 }.flatten.uniq
1108 1108 current -= last
1109 1109 current -= all
1110 1110 return true if current.include?(other)
1111 1111 last = current
1112 1112 all += last
1113 1113 end
1114 1114 false
1115 1115 end
1116 1116
1117 1117 # Returns an array of issues that duplicate this one
1118 1118 def duplicates
1119 1119 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1120 1120 end
1121 1121
1122 1122 # Returns the due date or the target due date if any
1123 1123 # Used on gantt chart
1124 1124 def due_before
1125 1125 due_date || (fixed_version ? fixed_version.effective_date : nil)
1126 1126 end
1127 1127
1128 1128 # Returns the time scheduled for this issue.
1129 1129 #
1130 1130 # Example:
1131 1131 # Start Date: 2/26/09, End Date: 3/04/09
1132 1132 # duration => 6
1133 1133 def duration
1134 1134 (start_date && due_date) ? due_date - start_date : 0
1135 1135 end
1136 1136
1137 1137 # Returns the duration in working days
1138 1138 def working_duration
1139 1139 (start_date && due_date) ? working_days(start_date, due_date) : 0
1140 1140 end
1141 1141
1142 1142 def soonest_start(reload=false)
1143 1143 if @soonest_start.nil? || reload
1144 1144 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1145 1145 p = @parent_issue || parent
1146 1146 if p && Setting.parent_issue_dates == 'derived'
1147 1147 dates << p.soonest_start
1148 1148 end
1149 1149 @soonest_start = dates.compact.max
1150 1150 end
1151 1151 @soonest_start
1152 1152 end
1153 1153
1154 1154 # Sets start_date on the given date or the next working day
1155 1155 # and changes due_date to keep the same working duration.
1156 1156 def reschedule_on(date)
1157 1157 wd = working_duration
1158 1158 date = next_working_date(date)
1159 1159 self.start_date = date
1160 1160 self.due_date = add_working_days(date, wd)
1161 1161 end
1162 1162
1163 1163 # Reschedules the issue on the given date or the next working day and saves the record.
1164 1164 # If the issue is a parent task, this is done by rescheduling its subtasks.
1165 1165 def reschedule_on!(date)
1166 1166 return if date.nil?
1167 1167 if leaf? || !dates_derived?
1168 1168 if start_date.nil? || start_date != date
1169 1169 if start_date && start_date > date
1170 1170 # Issue can not be moved earlier than its soonest start date
1171 1171 date = [soonest_start(true), date].compact.max
1172 1172 end
1173 1173 reschedule_on(date)
1174 1174 begin
1175 1175 save
1176 1176 rescue ActiveRecord::StaleObjectError
1177 1177 reload
1178 1178 reschedule_on(date)
1179 1179 save
1180 1180 end
1181 1181 end
1182 1182 else
1183 1183 leaves.each do |leaf|
1184 1184 if leaf.start_date
1185 1185 # Only move subtask if it starts at the same date as the parent
1186 1186 # or if it starts before the given date
1187 1187 if start_date == leaf.start_date || date > leaf.start_date
1188 1188 leaf.reschedule_on!(date)
1189 1189 end
1190 1190 else
1191 1191 leaf.reschedule_on!(date)
1192 1192 end
1193 1193 end
1194 1194 end
1195 1195 end
1196 1196
1197 1197 def dates_derived?
1198 1198 !leaf? && Setting.parent_issue_dates == 'derived'
1199 1199 end
1200 1200
1201 1201 def priority_derived?
1202 1202 !leaf? && Setting.parent_issue_priority == 'derived'
1203 1203 end
1204 1204
1205 1205 def done_ratio_derived?
1206 1206 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1207 1207 end
1208 1208
1209 1209 def <=>(issue)
1210 1210 if issue.nil?
1211 1211 -1
1212 1212 elsif root_id != issue.root_id
1213 1213 (root_id || 0) <=> (issue.root_id || 0)
1214 1214 else
1215 1215 (lft || 0) <=> (issue.lft || 0)
1216 1216 end
1217 1217 end
1218 1218
1219 1219 def to_s
1220 1220 "#{tracker} ##{id}: #{subject}"
1221 1221 end
1222 1222
1223 1223 # Returns a string of css classes that apply to the issue
1224 1224 def css_classes(user=User.current)
1225 1225 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1226 1226 s << ' closed' if closed?
1227 1227 s << ' overdue' if overdue?
1228 1228 s << ' child' if child?
1229 1229 s << ' parent' unless leaf?
1230 1230 s << ' private' if is_private?
1231 1231 if user.logged?
1232 1232 s << ' created-by-me' if author_id == user.id
1233 1233 s << ' assigned-to-me' if assigned_to_id == user.id
1234 1234 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1235 1235 end
1236 1236 s
1237 1237 end
1238 1238
1239 1239 # Unassigns issues from +version+ if it's no longer shared with issue's project
1240 1240 def self.update_versions_from_sharing_change(version)
1241 1241 # Update issues assigned to the version
1242 1242 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1243 1243 end
1244 1244
1245 1245 # Unassigns issues from versions that are no longer shared
1246 1246 # after +project+ was moved
1247 1247 def self.update_versions_from_hierarchy_change(project)
1248 1248 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1249 1249 # Update issues of the moved projects and issues assigned to a version of a moved project
1250 1250 Issue.update_versions(
1251 1251 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1252 1252 moved_project_ids, moved_project_ids]
1253 1253 )
1254 1254 end
1255 1255
1256 1256 def parent_issue_id=(arg)
1257 1257 s = arg.to_s.strip.presence
1258 1258 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1259 1259 @invalid_parent_issue_id = nil
1260 1260 elsif s.blank?
1261 1261 @parent_issue = nil
1262 1262 @invalid_parent_issue_id = nil
1263 1263 else
1264 1264 @parent_issue = nil
1265 1265 @invalid_parent_issue_id = arg
1266 1266 end
1267 1267 end
1268 1268
1269 1269 def parent_issue_id
1270 1270 if @invalid_parent_issue_id
1271 1271 @invalid_parent_issue_id
1272 1272 elsif instance_variable_defined? :@parent_issue
1273 1273 @parent_issue.nil? ? nil : @parent_issue.id
1274 1274 else
1275 1275 parent_id
1276 1276 end
1277 1277 end
1278 1278
1279 1279 def set_parent_id
1280 1280 self.parent_id = parent_issue_id
1281 1281 end
1282 1282
1283 1283 # Returns true if issue's project is a valid
1284 1284 # parent issue project
1285 1285 def valid_parent_project?(issue=parent)
1286 1286 return true if issue.nil? || issue.project_id == project_id
1287 1287
1288 1288 case Setting.cross_project_subtasks
1289 1289 when 'system'
1290 1290 true
1291 1291 when 'tree'
1292 1292 issue.project.root == project.root
1293 1293 when 'hierarchy'
1294 1294 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1295 1295 when 'descendants'
1296 1296 issue.project.is_or_is_ancestor_of?(project)
1297 1297 else
1298 1298 false
1299 1299 end
1300 1300 end
1301 1301
1302 1302 # Returns an issue scope based on project and scope
1303 1303 def self.cross_project_scope(project, scope=nil)
1304 1304 if project.nil?
1305 1305 return Issue
1306 1306 end
1307 1307 case scope
1308 1308 when 'all', 'system'
1309 1309 Issue
1310 1310 when 'tree'
1311 1311 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1312 1312 :lft => project.root.lft, :rgt => project.root.rgt)
1313 1313 when 'hierarchy'
1314 1314 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1315 1315 :lft => project.lft, :rgt => project.rgt)
1316 1316 when 'descendants'
1317 1317 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1318 1318 :lft => project.lft, :rgt => project.rgt)
1319 1319 else
1320 1320 Issue.where(:project_id => project.id)
1321 1321 end
1322 1322 end
1323 1323
1324 1324 def self.by_tracker(project)
1325 1325 count_and_group_by(:project => project, :association => :tracker)
1326 1326 end
1327 1327
1328 1328 def self.by_version(project)
1329 1329 count_and_group_by(:project => project, :association => :fixed_version)
1330 1330 end
1331 1331
1332 1332 def self.by_priority(project)
1333 1333 count_and_group_by(:project => project, :association => :priority)
1334 1334 end
1335 1335
1336 1336 def self.by_category(project)
1337 1337 count_and_group_by(:project => project, :association => :category)
1338 1338 end
1339 1339
1340 1340 def self.by_assigned_to(project)
1341 1341 count_and_group_by(:project => project, :association => :assigned_to)
1342 1342 end
1343 1343
1344 1344 def self.by_author(project)
1345 1345 count_and_group_by(:project => project, :association => :author)
1346 1346 end
1347 1347
1348 1348 def self.by_subproject(project)
1349 1349 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1350 1350 r.reject {|r| r["project_id"] == project.id.to_s}
1351 1351 end
1352 1352
1353 1353 # Query generator for selecting groups of issue counts for a project
1354 1354 # based on specific criteria
1355 1355 #
1356 1356 # Options
1357 1357 # * project - Project to search in.
1358 1358 # * with_subprojects - Includes subprojects issues if set to true.
1359 1359 # * association - Symbol. Association for grouping.
1360 1360 def self.count_and_group_by(options)
1361 1361 assoc = reflect_on_association(options[:association])
1362 1362 select_field = assoc.foreign_key
1363 1363
1364 1364 Issue.
1365 1365 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1366 1366 joins(:status, assoc.name).
1367 1367 group(:status_id, :is_closed, select_field).
1368 1368 count.
1369 1369 map do |columns, total|
1370 1370 status_id, is_closed, field_value = columns
1371 1371 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1372 1372 {
1373 1373 "status_id" => status_id.to_s,
1374 1374 "closed" => is_closed,
1375 1375 select_field => field_value.to_s,
1376 1376 "total" => total.to_s
1377 1377 }
1378 1378 end
1379 1379 end
1380 1380
1381 1381 # Returns a scope of projects that user can assign the issue to
1382 1382 def allowed_target_projects(user=User.current)
1383 1383 current_project = new_record? ? nil : project
1384 1384 self.class.allowed_target_projects(user, current_project)
1385 1385 end
1386 1386
1387 1387 # Returns a scope of projects that user can assign issues to
1388 1388 # If current_project is given, it will be included in the scope
1389 1389 def self.allowed_target_projects(user=User.current, current_project=nil)
1390 1390 condition = Project.allowed_to_condition(user, :add_issues)
1391 1391 if current_project
1392 1392 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1393 1393 end
1394 1394 Project.where(condition).having_trackers
1395 1395 end
1396 1396
1397 1397 # Returns a scope of trackers that user can assign the issue to
1398 1398 def allowed_target_trackers(user=User.current)
1399 1399 self.class.allowed_target_trackers(project, user, tracker_id_was)
1400 1400 end
1401 1401
1402 1402 # Returns a scope of trackers that user can assign project issues to
1403 1403 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1404 1404 if project
1405 1405 scope = project.trackers.sorted
1406 1406 unless user.admin?
1407 1407 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1408 1408 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1409 1409 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1410 1410 if current_tracker
1411 1411 tracker_ids << current_tracker
1412 1412 end
1413 1413 scope = scope.where(:id => tracker_ids)
1414 1414 end
1415 1415 end
1416 1416 scope
1417 1417 else
1418 1418 Tracker.none
1419 1419 end
1420 1420 end
1421 1421
1422 1422 private
1423 1423
1424 1424 def user_tracker_permission?(user, permission)
1425 1425 if user.admin?
1426 1426 true
1427 1427 else
1428 1428 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1429 1429 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1430 1430 end
1431 1431 end
1432 1432
1433 1433 def after_project_change
1434 1434 # Update project_id on related time entries
1435 1435 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1436 1436
1437 1437 # Delete issue relations
1438 1438 unless Setting.cross_project_issue_relations?
1439 1439 relations_from.clear
1440 1440 relations_to.clear
1441 1441 end
1442 1442
1443 1443 # Move subtasks that were in the same project
1444 1444 children.each do |child|
1445 1445 next unless child.project_id == project_id_was
1446 1446 # Change project and keep project
1447 1447 child.send :project=, project, true
1448 1448 unless child.save
1449 1449 raise ActiveRecord::Rollback
1450 1450 end
1451 1451 end
1452 1452 end
1453 1453
1454 1454 # Callback for after the creation of an issue by copy
1455 1455 # * adds a "copied to" relation with the copied issue
1456 1456 # * copies subtasks from the copied issue
1457 1457 def after_create_from_copy
1458 1458 return unless copy? && !@after_create_from_copy_handled
1459 1459
1460 1460 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1461 1461 if @current_journal
1462 1462 @copied_from.init_journal(@current_journal.user)
1463 1463 end
1464 1464 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1465 1465 unless relation.save
1466 1466 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1467 1467 end
1468 1468 end
1469 1469
1470 1470 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1471 1471 copy_options = (@copy_options || {}).merge(:subtasks => false)
1472 1472 copied_issue_ids = {@copied_from.id => self.id}
1473 1473 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1474 1474 # Do not copy self when copying an issue as a descendant of the copied issue
1475 1475 next if child == self
1476 1476 # Do not copy subtasks of issues that were not copied
1477 1477 next unless copied_issue_ids[child.parent_id]
1478 1478 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1479 1479 unless child.visible?
1480 1480 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1481 1481 next
1482 1482 end
1483 1483 copy = Issue.new.copy_from(child, copy_options)
1484 1484 if @current_journal
1485 1485 copy.init_journal(@current_journal.user)
1486 1486 end
1487 1487 copy.author = author
1488 1488 copy.project = project
1489 1489 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1490 1490 unless copy.save
1491 1491 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1492 1492 next
1493 1493 end
1494 1494 copied_issue_ids[child.id] = copy.id
1495 1495 end
1496 1496 end
1497 1497 @after_create_from_copy_handled = true
1498 1498 end
1499 1499
1500 1500 def update_nested_set_attributes
1501 1501 if parent_id_changed?
1502 1502 update_nested_set_attributes_on_parent_change
1503 1503 end
1504 1504 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1505 1505 end
1506 1506
1507 1507 # Updates the nested set for when an existing issue is moved
1508 1508 def update_nested_set_attributes_on_parent_change
1509 1509 former_parent_id = parent_id_was
1510 1510 # delete invalid relations of all descendants
1511 1511 self_and_descendants.each do |issue|
1512 1512 issue.relations.each do |relation|
1513 1513 relation.destroy unless relation.valid?
1514 1514 end
1515 1515 end
1516 1516 # update former parent
1517 1517 recalculate_attributes_for(former_parent_id) if former_parent_id
1518 1518 end
1519 1519
1520 1520 def update_parent_attributes
1521 1521 if parent_id
1522 1522 recalculate_attributes_for(parent_id)
1523 1523 association(:parent).reset
1524 1524 end
1525 1525 end
1526 1526
1527 1527 def recalculate_attributes_for(issue_id)
1528 1528 if issue_id && p = Issue.find_by_id(issue_id)
1529 1529 if p.priority_derived?
1530 1530 # priority = highest priority of open children
1531 1531 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1532 1532 p.priority = IssuePriority.find_by_position(priority_position)
1533 1533 else
1534 1534 p.priority = IssuePriority.default
1535 1535 end
1536 1536 end
1537 1537
1538 1538 if p.dates_derived?
1539 1539 # start/due dates = lowest/highest dates of children
1540 1540 p.start_date = p.children.minimum(:start_date)
1541 1541 p.due_date = p.children.maximum(:due_date)
1542 1542 if p.start_date && p.due_date && p.due_date < p.start_date
1543 1543 p.start_date, p.due_date = p.due_date, p.start_date
1544 1544 end
1545 1545 end
1546 1546
1547 1547 if p.done_ratio_derived?
1548 1548 # done ratio = weighted average ratio of leaves
1549 1549 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1550 1550 child_count = p.children.count
1551 1551 if child_count > 0
1552 1552 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1553 1553 if average == 0
1554 1554 average = 1
1555 1555 end
1556 1556 done = p.children.joins(:status).
1557 1557 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1558 1558 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1559 1559 progress = done / (average * child_count)
1560 1560 p.done_ratio = progress.round
1561 1561 end
1562 1562 end
1563 1563 end
1564 1564
1565 1565 # ancestors will be recursively updated
1566 1566 p.save(:validate => false)
1567 1567 end
1568 1568 end
1569 1569
1570 1570 # Update issues so their versions are not pointing to a
1571 1571 # fixed_version that is not shared with the issue's project
1572 1572 def self.update_versions(conditions=nil)
1573 1573 # Only need to update issues with a fixed_version from
1574 1574 # a different project and that is not systemwide shared
1575 1575 Issue.joins(:project, :fixed_version).
1576 1576 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1577 1577 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1578 1578 " AND #{Version.table_name}.sharing <> 'system'").
1579 1579 where(conditions).each do |issue|
1580 1580 next if issue.project.nil? || issue.fixed_version.nil?
1581 1581 unless issue.project.shared_versions.include?(issue.fixed_version)
1582 1582 issue.init_journal(User.current)
1583 1583 issue.fixed_version = nil
1584 1584 issue.save
1585 1585 end
1586 1586 end
1587 1587 end
1588 1588
1589 1589 # Callback on file attachment
1590 1590 def attachment_added(attachment)
1591 1591 if current_journal && !attachment.new_record?
1592 1592 current_journal.journalize_attachment(attachment, :added)
1593 1593 end
1594 1594 end
1595 1595
1596 1596 # Callback on attachment deletion
1597 1597 def attachment_removed(attachment)
1598 1598 if current_journal && !attachment.new_record?
1599 1599 current_journal.journalize_attachment(attachment, :removed)
1600 1600 current_journal.save
1601 1601 end
1602 1602 end
1603 1603
1604 1604 # Called after a relation is added
1605 1605 def relation_added(relation)
1606 1606 if current_journal
1607 1607 current_journal.journalize_relation(relation, :added)
1608 1608 current_journal.save
1609 1609 end
1610 1610 end
1611 1611
1612 1612 # Called after a relation is removed
1613 1613 def relation_removed(relation)
1614 1614 if current_journal
1615 1615 current_journal.journalize_relation(relation, :removed)
1616 1616 current_journal.save
1617 1617 end
1618 1618 end
1619 1619
1620 1620 # Default assignment based on category
1621 1621 def default_assign
1622 1622 if assigned_to.nil? && category && category.assigned_to
1623 1623 self.assigned_to = category.assigned_to
1624 1624 end
1625 1625 end
1626 1626
1627 1627 # Updates start/due dates of following issues
1628 1628 def reschedule_following_issues
1629 1629 if start_date_changed? || due_date_changed?
1630 1630 relations_from.each do |relation|
1631 1631 relation.set_issue_to_dates
1632 1632 end
1633 1633 end
1634 1634 end
1635 1635
1636 1636 # Closes duplicates if the issue is being closed
1637 1637 def close_duplicates
1638 1638 if closing?
1639 1639 duplicates.each do |duplicate|
1640 1640 # Reload is needed in case the duplicate was updated by a previous duplicate
1641 1641 duplicate.reload
1642 1642 # Don't re-close it if it's already closed
1643 1643 next if duplicate.closed?
1644 1644 # Same user and notes
1645 1645 if @current_journal
1646 1646 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1647 1647 duplicate.private_notes = @current_journal.private_notes
1648 1648 end
1649 1649 duplicate.update_attribute :status, self.status
1650 1650 end
1651 1651 end
1652 1652 end
1653 1653
1654 1654 # Make sure updated_on is updated when adding a note and set updated_on now
1655 1655 # so we can set closed_on with the same value on closing
1656 1656 def force_updated_on_change
1657 1657 if @current_journal || changed?
1658 1658 self.updated_on = current_time_from_proper_timezone
1659 1659 if new_record?
1660 1660 self.created_on = updated_on
1661 1661 end
1662 1662 end
1663 1663 end
1664 1664
1665 1665 # Callback for setting closed_on when the issue is closed.
1666 1666 # The closed_on attribute stores the time of the last closing
1667 1667 # and is preserved when the issue is reopened.
1668 1668 def update_closed_on
1669 1669 if closing?
1670 1670 self.closed_on = updated_on
1671 1671 end
1672 1672 end
1673 1673
1674 1674 # Saves the changes in a Journal
1675 1675 # Called after_save
1676 1676 def create_journal
1677 1677 if current_journal
1678 1678 current_journal.save
1679 1679 end
1680 1680 end
1681 1681
1682 1682 def send_notification
1683 1683 if notify? && Setting.notified_events.include?('issue_added')
1684 1684 Mailer.deliver_issue_add(self)
1685 1685 end
1686 1686 end
1687 1687
1688 1688 # Stores the previous assignee so we can still have access
1689 1689 # to it during after_save callbacks (assigned_to_id_was is reset)
1690 1690 def set_assigned_to_was
1691 1691 @previous_assigned_to_id = assigned_to_id_was
1692 1692 end
1693 1693
1694 1694 # Clears the previous assignee at the end of after_save callbacks
1695 1695 def clear_assigned_to_was
1696 1696 @assigned_to_was = nil
1697 1697 @previous_assigned_to_id = nil
1698 1698 end
1699 1699
1700 1700 def clear_disabled_fields
1701 1701 if tracker
1702 1702 tracker.disabled_core_fields.each do |attribute|
1703 1703 send "#{attribute}=", nil
1704 1704 end
1705 1705 self.done_ratio ||= 0
1706 1706 end
1707 1707 end
1708 1708 end
@@ -1,1055 +1,1066
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::NestedSet::ProjectNestedSet
21 21
22 22 # Project statuses
23 23 STATUS_ACTIVE = 1
24 24 STATUS_CLOSED = 5
25 25 STATUS_ARCHIVED = 9
26 26
27 27 # Maximum length for project identifiers
28 28 IDENTIFIER_MAX_LENGTH = 100
29 29
30 30 # Specific overridden Activities
31 31 has_many :time_entry_activities
32 32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
33 33 # Memberships of active users only
34 34 has_many :members,
35 35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
36 36 has_many :enabled_modules, :dependent => :delete_all
37 37 has_and_belongs_to_many :trackers, lambda {order(:position)}
38 38 has_many :issues, :dependent => :destroy
39 39 has_many :issue_changes, :through => :issues, :source => :journals
40 40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 41 belongs_to :default_version, :class_name => 'Version'
42 42 has_many :time_entries, :dependent => :destroy
43 43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 44 has_many :documents, :dependent => :destroy
45 45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
46 46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
47 47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
48 48 has_one :repository, lambda {where(["is_default = ?", true])}
49 49 has_many :repositories, :dependent => :destroy
50 50 has_many :changesets, :through => :repository
51 51 has_one :wiki, :dependent => :destroy
52 52 # Custom field for the project issues
53 53 has_and_belongs_to_many :issue_custom_fields,
54 54 lambda {order("#{CustomField.table_name}.position")},
55 55 :class_name => 'IssueCustomField',
56 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 57 :association_foreign_key => 'custom_field_id'
58 58
59 59 acts_as_attachable :view_permission => :view_files,
60 60 :edit_permission => :manage_files,
61 61 :delete_permission => :manage_files
62 62
63 63 acts_as_customizable
64 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
65 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 67 :author => nil
68 68
69 69 attr_protected :status
70 70
71 71 validates_presence_of :name, :identifier
72 72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
73 73 validates_length_of :name, :maximum => 255
74 74 validates_length_of :homepage, :maximum => 255
75 75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 76 # downcase letters, digits, dashes but not digits only
77 77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 78 # reserved words
79 79 validates_exclusion_of :identifier, :in => %w( new )
80 80 validate :validate_parent
81 81
82 82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
84 84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
85 85 before_destroy :delete_all_members
86 86
87 87 scope :has_module, lambda {|mod|
88 88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 89 }
90 90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 92 scope :all_public, lambda { where(:is_public => true) }
93 93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 94 scope :allowed_to, lambda {|*args|
95 95 user = User.current
96 96 permission = nil
97 97 if args.first.is_a?(Symbol)
98 98 permission = args.shift
99 99 else
100 100 user = args.shift
101 101 permission = args.shift
102 102 end
103 103 where(Project.allowed_to_condition(user, permission, *args))
104 104 }
105 105 scope :like, lambda {|arg|
106 106 if arg.blank?
107 107 where(nil)
108 108 else
109 109 pattern = "%#{arg.to_s.strip.downcase}%"
110 110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 111 end
112 112 }
113 113 scope :sorted, lambda {order(:lft)}
114 114 scope :having_trackers, lambda {
115 115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
116 116 }
117 117
118 118 def initialize(attributes=nil, *args)
119 119 super
120 120
121 121 initialized = (attributes || {}).stringify_keys
122 122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
123 123 self.identifier = Project.next_identifier
124 124 end
125 125 if !initialized.key?('is_public')
126 126 self.is_public = Setting.default_projects_public?
127 127 end
128 128 if !initialized.key?('enabled_module_names')
129 129 self.enabled_module_names = Setting.default_projects_modules
130 130 end
131 131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
132 132 default = Setting.default_projects_tracker_ids
133 133 if default.is_a?(Array)
134 134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
135 135 else
136 136 self.trackers = Tracker.sorted.to_a
137 137 end
138 138 end
139 139 end
140 140
141 141 def identifier=(identifier)
142 142 super unless identifier_frozen?
143 143 end
144 144
145 145 def identifier_frozen?
146 146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
147 147 end
148 148
149 149 # returns latest created projects
150 150 # non public projects will be returned only if user is a member of those
151 151 def self.latest(user=nil, count=5)
152 152 visible(user).limit(count).
153 153 order(:created_on => :desc).
154 154 where("#{table_name}.created_on >= ?", 30.days.ago).
155 155 to_a
156 156 end
157 157
158 158 # Returns true if the project is visible to +user+ or to the current user.
159 159 def visible?(user=User.current)
160 160 user.allowed_to?(:view_project, self)
161 161 end
162 162
163 163 # Returns a SQL conditions string used to find all projects visible by the specified user.
164 164 #
165 165 # Examples:
166 166 # Project.visible_condition(admin) => "projects.status = 1"
167 167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
168 168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
169 169 def self.visible_condition(user, options={})
170 170 allowed_to_condition(user, :view_project, options)
171 171 end
172 172
173 173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
174 174 #
175 175 # Valid options:
176 176 # * :project => limit the condition to project
177 177 # * :with_subprojects => limit the condition to project and its subprojects
178 178 # * :member => limit the condition to the user projects
179 179 def self.allowed_to_condition(user, permission, options={})
180 180 perm = Redmine::AccessControl.permission(permission)
181 181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
182 182 if perm && perm.project_module
183 183 # If the permission belongs to a project module, make sure the module is enabled
184 184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
185 185 end
186 186 if project = options[:project]
187 187 project_statement = project.project_condition(options[:with_subprojects])
188 188 base_statement = "(#{project_statement}) AND (#{base_statement})"
189 189 end
190 190
191 191 if user.admin?
192 192 base_statement
193 193 else
194 194 statement_by_role = {}
195 195 unless options[:member]
196 196 role = user.builtin_role
197 197 if role.allowed_to?(permission)
198 198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
199 199 if user.id
200 200 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
201 201 end
202 202 statement_by_role[role] = s
203 203 end
204 204 end
205 205 user.projects_by_role.each do |role, projects|
206 206 if role.allowed_to?(permission) && projects.any?
207 207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
208 208 end
209 209 end
210 210 if statement_by_role.empty?
211 211 "1=0"
212 212 else
213 213 if block_given?
214 214 statement_by_role.each do |role, statement|
215 215 if s = yield(role, user)
216 216 statement_by_role[role] = "(#{statement} AND (#{s}))"
217 217 end
218 218 end
219 219 end
220 220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
221 221 end
222 222 end
223 223 end
224 224
225 225 def override_roles(role)
226 226 @override_members ||= memberships.
227 227 joins(:principal).
228 228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
229 229
230 230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
231 231 member = @override_members.detect {|m| m.principal.is_a? group_class}
232 232 member ? member.roles.to_a : [role]
233 233 end
234 234
235 235 def principals
236 236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
237 237 end
238 238
239 239 def users
240 240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
241 241 end
242 242
243 243 # Returns the Systemwide and project specific activities
244 244 def activities(include_inactive=false)
245 245 t = TimeEntryActivity.table_name
246 246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
247 247
248 248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
249 249 if overridden_activity_ids.any?
250 250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
251 251 end
252 252 unless include_inactive
253 253 scope = scope.active
254 254 end
255 255 scope
256 256 end
257 257
258 258 # Will create a new Project specific Activity or update an existing one
259 259 #
260 260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
261 261 # does not successfully save.
262 262 def update_or_create_time_entry_activity(id, activity_hash)
263 263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
264 264 self.create_time_entry_activity_if_needed(activity_hash)
265 265 else
266 266 activity = project.time_entry_activities.find_by_id(id.to_i)
267 267 activity.update_attributes(activity_hash) if activity
268 268 end
269 269 end
270 270
271 271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
272 272 #
273 273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
274 274 # does not successfully save.
275 275 def create_time_entry_activity_if_needed(activity)
276 276 if activity['parent_id']
277 277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
278 278 activity['name'] = parent_activity.name
279 279 activity['position'] = parent_activity.position
280 280 if Enumeration.overriding_change?(activity, parent_activity)
281 281 project_activity = self.time_entry_activities.create(activity)
282 282 if project_activity.new_record?
283 283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
284 284 else
285 285 self.time_entries.
286 286 where(:activity_id => parent_activity.id).
287 287 update_all(:activity_id => project_activity.id)
288 288 end
289 289 end
290 290 end
291 291 end
292 292
293 293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
294 294 #
295 295 # Examples:
296 296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
297 297 # project.project_condition(false) => "projects.id = 1"
298 298 def project_condition(with_subprojects)
299 299 cond = "#{Project.table_name}.id = #{id}"
300 300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
301 301 cond
302 302 end
303 303
304 304 def self.find(*args)
305 305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
306 306 project = find_by_identifier(*args)
307 307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
308 308 project
309 309 else
310 310 super
311 311 end
312 312 end
313 313
314 314 def self.find_by_param(*args)
315 315 self.find(*args)
316 316 end
317 317
318 318 alias :base_reload :reload
319 319 def reload(*args)
320 320 @principals = nil
321 321 @users = nil
322 322 @shared_versions = nil
323 323 @rolled_up_versions = nil
324 324 @rolled_up_trackers = nil
325 325 @all_issue_custom_fields = nil
326 326 @all_time_entry_custom_fields = nil
327 327 @to_param = nil
328 328 @allowed_parents = nil
329 329 @allowed_permissions = nil
330 330 @actions_allowed = nil
331 331 @start_date = nil
332 332 @due_date = nil
333 333 @override_members = nil
334 334 @assignable_users = nil
335 335 base_reload(*args)
336 336 end
337 337
338 338 def to_param
339 339 if new_record?
340 340 nil
341 341 else
342 342 # id is used for projects with a numeric identifier (compatibility)
343 343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
344 344 end
345 345 end
346 346
347 347 def active?
348 348 self.status == STATUS_ACTIVE
349 349 end
350 350
351 351 def archived?
352 352 self.status == STATUS_ARCHIVED
353 353 end
354 354
355 355 # Archives the project and its descendants
356 356 def archive
357 357 # Check that there is no issue of a non descendant project that is assigned
358 358 # to one of the project or descendant versions
359 359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
360 360
361 361 if version_ids.any? &&
362 362 Issue.
363 363 includes(:project).
364 364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
365 365 where(:fixed_version_id => version_ids).
366 366 exists?
367 367 return false
368 368 end
369 369 Project.transaction do
370 370 archive!
371 371 end
372 372 true
373 373 end
374 374
375 375 # Unarchives the project
376 376 # All its ancestors must be active
377 377 def unarchive
378 378 return false if ancestors.detect {|a| !a.active?}
379 379 update_attribute :status, STATUS_ACTIVE
380 380 end
381 381
382 382 def close
383 383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
384 384 end
385 385
386 386 def reopen
387 387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
388 388 end
389 389
390 390 # Returns an array of projects the project can be moved to
391 391 # by the current user
392 392 def allowed_parents(user=User.current)
393 393 return @allowed_parents if @allowed_parents
394 394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
395 395 @allowed_parents = @allowed_parents - self_and_descendants
396 396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
397 397 @allowed_parents << nil
398 398 end
399 399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
400 400 @allowed_parents << parent
401 401 end
402 402 @allowed_parents
403 403 end
404 404
405 405 # Sets the parent of the project with authorization check
406 406 def set_allowed_parent!(p)
407 407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
408 408 p = p.id if p.is_a?(Project)
409 409 send :safe_attributes, {:project_id => p}
410 410 save
411 411 end
412 412
413 413 # Sets the parent of the project and saves the project
414 414 # Argument can be either a Project, a String, a Fixnum or nil
415 415 def set_parent!(p)
416 416 if p.is_a?(Project)
417 417 self.parent = p
418 418 else
419 419 self.parent_id = p
420 420 end
421 421 save
422 422 end
423 423
424 424 # Returns a scope of the trackers used by the project and its active sub projects
425 425 def rolled_up_trackers(include_subprojects=true)
426 426 if include_subprojects
427 427 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
428 428 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
429 429 else
430 430 rolled_up_trackers_base_scope.
431 431 where(:projects => {:id => id})
432 432 end
433 433 end
434 434
435 435 def rolled_up_trackers_base_scope
436 436 Tracker.
437 437 joins(projects: :enabled_modules).
438 438 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
439 439 where(:enabled_modules => {:name => 'issue_tracking'}).
440 440 uniq.
441 441 sorted
442 442 end
443 443
444 444 # Closes open and locked project versions that are completed
445 445 def close_completed_versions
446 446 Version.transaction do
447 447 versions.where(:status => %w(open locked)).each do |version|
448 448 if version.completed?
449 449 version.update_attribute(:status, 'closed')
450 450 end
451 451 end
452 452 end
453 453 end
454 454
455 455 # Returns a scope of the Versions on subprojects
456 456 def rolled_up_versions
457 457 @rolled_up_versions ||=
458 458 Version.
459 459 joins(:project).
460 460 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
461 461 end
462 462
463 463 # Returns a scope of the Versions used by the project
464 464 def shared_versions
465 465 if new_record?
466 466 Version.
467 467 joins(:project).
468 468 preload(:project).
469 469 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
470 470 else
471 471 @shared_versions ||= begin
472 472 r = root? ? self : root
473 473 Version.
474 474 joins(:project).
475 475 preload(:project).
476 476 where("#{Project.table_name}.id = #{id}" +
477 477 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
478 478 " #{Version.table_name}.sharing = 'system'" +
479 479 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
480 480 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
481 481 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
482 482 "))")
483 483 end
484 484 end
485 485 end
486 486
487 487 # Returns a hash of project users grouped by role
488 488 def users_by_role
489 489 members.includes(:user, :roles).inject({}) do |h, m|
490 490 m.roles.each do |r|
491 491 h[r] ||= []
492 492 h[r] << m.user
493 493 end
494 494 h
495 495 end
496 496 end
497 497
498 498 # Adds user as a project member with the default role
499 499 # Used for when a non-admin user creates a project
500 500 def add_default_member(user)
501 501 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
502 502 member = Member.new(:project => self, :principal => user, :roles => [role])
503 503 self.members << member
504 504 member
505 505 end
506 506
507 507 # Deletes all project's members
508 508 def delete_all_members
509 509 me, mr = Member.table_name, MemberRole.table_name
510 510 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
511 511 Member.delete_all(['project_id = ?', id])
512 512 end
513 513
514 514 # Return a Principal scope of users/groups issues can be assigned to
515 def assignable_users
515 def assignable_users(tracker=nil)
516 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
517
516 518 types = ['User']
517 519 types << 'Group' if Setting.issue_group_assignment?
518 520
519 @assignable_users ||= Principal.
521 scope = Principal.
520 522 active.
521 523 joins(:members => :roles).
522 524 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
523 525 uniq.
524 526 sorted
527
528 if tracker
529 # Rejects users that cannot the view the tracker
530 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
531 scope = scope.where(:roles => {:id => roles.map(&:id)})
532 end
533
534 @assignable_users ||= {}
535 @assignable_users[tracker] = scope
525 536 end
526 537
527 538 # Returns the mail addresses of users that should be always notified on project events
528 539 def recipients
529 540 notified_users.collect {|user| user.mail}
530 541 end
531 542
532 543 # Returns the users that should be notified on project events
533 544 def notified_users
534 545 # TODO: User part should be extracted to User#notify_about?
535 546 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
536 547 end
537 548
538 549 # Returns a scope of all custom fields enabled for project issues
539 550 # (explicitly associated custom fields and custom fields enabled for all projects)
540 551 def all_issue_custom_fields
541 552 if new_record?
542 553 @all_issue_custom_fields ||= IssueCustomField.
543 554 sorted.
544 555 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
545 556 else
546 557 @all_issue_custom_fields ||= IssueCustomField.
547 558 sorted.
548 559 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
549 560 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
550 561 " WHERE cfp.project_id = ?)", true, id)
551 562 end
552 563 end
553 564
554 565 def project
555 566 self
556 567 end
557 568
558 569 def <=>(project)
559 570 name.casecmp(project.name)
560 571 end
561 572
562 573 def to_s
563 574 name
564 575 end
565 576
566 577 # Returns a short description of the projects (first lines)
567 578 def short_description(length = 255)
568 579 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
569 580 end
570 581
571 582 def css_classes
572 583 s = 'project'
573 584 s << ' root' if root?
574 585 s << ' child' if child?
575 586 s << (leaf? ? ' leaf' : ' parent')
576 587 unless active?
577 588 if archived?
578 589 s << ' archived'
579 590 else
580 591 s << ' closed'
581 592 end
582 593 end
583 594 s
584 595 end
585 596
586 597 # The earliest start date of a project, based on it's issues and versions
587 598 def start_date
588 599 @start_date ||= [
589 600 issues.minimum('start_date'),
590 601 shared_versions.minimum('effective_date'),
591 602 Issue.fixed_version(shared_versions).minimum('start_date')
592 603 ].compact.min
593 604 end
594 605
595 606 # The latest due date of an issue or version
596 607 def due_date
597 608 @due_date ||= [
598 609 issues.maximum('due_date'),
599 610 shared_versions.maximum('effective_date'),
600 611 Issue.fixed_version(shared_versions).maximum('due_date')
601 612 ].compact.max
602 613 end
603 614
604 615 def overdue?
605 616 active? && !due_date.nil? && (due_date < User.current.today)
606 617 end
607 618
608 619 # Returns the percent completed for this project, based on the
609 620 # progress on it's versions.
610 621 def completed_percent(options={:include_subprojects => false})
611 622 if options.delete(:include_subprojects)
612 623 total = self_and_descendants.collect(&:completed_percent).sum
613 624
614 625 total / self_and_descendants.count
615 626 else
616 627 if versions.count > 0
617 628 total = versions.collect(&:completed_percent).sum
618 629
619 630 total / versions.count
620 631 else
621 632 100
622 633 end
623 634 end
624 635 end
625 636
626 637 # Return true if this project allows to do the specified action.
627 638 # action can be:
628 639 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
629 640 # * a permission Symbol (eg. :edit_project)
630 641 def allows_to?(action)
631 642 if archived?
632 643 # No action allowed on archived projects
633 644 return false
634 645 end
635 646 unless active? || Redmine::AccessControl.read_action?(action)
636 647 # No write action allowed on closed projects
637 648 return false
638 649 end
639 650 # No action allowed on disabled modules
640 651 if action.is_a? Hash
641 652 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
642 653 else
643 654 allowed_permissions.include? action
644 655 end
645 656 end
646 657
647 658 # Return the enabled module with the given name
648 659 # or nil if the module is not enabled for the project
649 660 def enabled_module(name)
650 661 name = name.to_s
651 662 enabled_modules.detect {|m| m.name == name}
652 663 end
653 664
654 665 # Return true if the module with the given name is enabled
655 666 def module_enabled?(name)
656 667 enabled_module(name).present?
657 668 end
658 669
659 670 def enabled_module_names=(module_names)
660 671 if module_names && module_names.is_a?(Array)
661 672 module_names = module_names.collect(&:to_s).reject(&:blank?)
662 673 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
663 674 else
664 675 enabled_modules.clear
665 676 end
666 677 end
667 678
668 679 # Returns an array of the enabled modules names
669 680 def enabled_module_names
670 681 enabled_modules.collect(&:name)
671 682 end
672 683
673 684 # Enable a specific module
674 685 #
675 686 # Examples:
676 687 # project.enable_module!(:issue_tracking)
677 688 # project.enable_module!("issue_tracking")
678 689 def enable_module!(name)
679 690 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
680 691 end
681 692
682 693 # Disable a module if it exists
683 694 #
684 695 # Examples:
685 696 # project.disable_module!(:issue_tracking)
686 697 # project.disable_module!("issue_tracking")
687 698 # project.disable_module!(project.enabled_modules.first)
688 699 def disable_module!(target)
689 700 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
690 701 target.destroy unless target.blank?
691 702 end
692 703
693 704 safe_attributes 'name',
694 705 'description',
695 706 'homepage',
696 707 'is_public',
697 708 'identifier',
698 709 'custom_field_values',
699 710 'custom_fields',
700 711 'tracker_ids',
701 712 'issue_custom_field_ids',
702 713 'parent_id',
703 714 'default_version_id'
704 715
705 716 safe_attributes 'enabled_module_names',
706 717 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
707 718
708 719 safe_attributes 'inherit_members',
709 720 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
710 721
711 722 def safe_attributes=(attrs, user=User.current)
712 723 return unless attrs.is_a?(Hash)
713 724 attrs = attrs.deep_dup
714 725
715 726 @unallowed_parent_id = nil
716 727 if new_record? || attrs.key?('parent_id')
717 728 parent_id_param = attrs['parent_id'].to_s
718 729 if new_record? || parent_id_param != parent_id.to_s
719 730 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
720 731 unless allowed_parents(user).include?(p)
721 732 attrs.delete('parent_id')
722 733 @unallowed_parent_id = true
723 734 end
724 735 end
725 736 end
726 737
727 738 super(attrs, user)
728 739 end
729 740
730 741 # Returns an auto-generated project identifier based on the last identifier used
731 742 def self.next_identifier
732 743 p = Project.order('id DESC').first
733 744 p.nil? ? nil : p.identifier.to_s.succ
734 745 end
735 746
736 747 # Copies and saves the Project instance based on the +project+.
737 748 # Duplicates the source project's:
738 749 # * Wiki
739 750 # * Versions
740 751 # * Categories
741 752 # * Issues
742 753 # * Members
743 754 # * Queries
744 755 #
745 756 # Accepts an +options+ argument to specify what to copy
746 757 #
747 758 # Examples:
748 759 # project.copy(1) # => copies everything
749 760 # project.copy(1, :only => 'members') # => copies members only
750 761 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
751 762 def copy(project, options={})
752 763 project = project.is_a?(Project) ? project : Project.find(project)
753 764
754 765 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
755 766 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
756 767
757 768 Project.transaction do
758 769 if save
759 770 reload
760 771 to_be_copied.each do |name|
761 772 send "copy_#{name}", project
762 773 end
763 774 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
764 775 save
765 776 else
766 777 false
767 778 end
768 779 end
769 780 end
770 781
771 782 def member_principals
772 783 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
773 784 memberships.active
774 785 end
775 786
776 787 # Returns a new unsaved Project instance with attributes copied from +project+
777 788 def self.copy_from(project)
778 789 project = project.is_a?(Project) ? project : Project.find(project)
779 790 # clear unique attributes
780 791 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
781 792 copy = Project.new(attributes)
782 793 copy.enabled_module_names = project.enabled_module_names
783 794 copy.trackers = project.trackers
784 795 copy.custom_values = project.custom_values.collect {|v| v.clone}
785 796 copy.issue_custom_fields = project.issue_custom_fields
786 797 copy
787 798 end
788 799
789 800 # Yields the given block for each project with its level in the tree
790 801 def self.project_tree(projects, &block)
791 802 ancestors = []
792 803 projects.sort_by(&:lft).each do |project|
793 804 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
794 805 ancestors.pop
795 806 end
796 807 yield project, ancestors.size
797 808 ancestors << project
798 809 end
799 810 end
800 811
801 812 private
802 813
803 814 def update_inherited_members
804 815 if parent
805 816 if inherit_members? && !inherit_members_was
806 817 remove_inherited_member_roles
807 818 add_inherited_member_roles
808 819 elsif !inherit_members? && inherit_members_was
809 820 remove_inherited_member_roles
810 821 end
811 822 end
812 823 end
813 824
814 825 def remove_inherited_member_roles
815 826 member_roles = memberships.map(&:member_roles).flatten
816 827 member_role_ids = member_roles.map(&:id)
817 828 member_roles.each do |member_role|
818 829 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
819 830 member_role.destroy
820 831 end
821 832 end
822 833 end
823 834
824 835 def add_inherited_member_roles
825 836 if inherit_members? && parent
826 837 parent.memberships.each do |parent_member|
827 838 member = Member.find_or_new(self.id, parent_member.user_id)
828 839 parent_member.member_roles.each do |parent_member_role|
829 840 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
830 841 end
831 842 member.save!
832 843 end
833 844 memberships.reset
834 845 end
835 846 end
836 847
837 848 def update_versions_from_hierarchy_change
838 849 Issue.update_versions_from_hierarchy_change(self)
839 850 end
840 851
841 852 def validate_parent
842 853 if @unallowed_parent_id
843 854 errors.add(:parent_id, :invalid)
844 855 elsif parent_id_changed?
845 856 unless parent.nil? || (parent.active? && move_possible?(parent))
846 857 errors.add(:parent_id, :invalid)
847 858 end
848 859 end
849 860 end
850 861
851 862 # Copies wiki from +project+
852 863 def copy_wiki(project)
853 864 # Check that the source project has a wiki first
854 865 unless project.wiki.nil?
855 866 wiki = self.wiki || Wiki.new
856 867 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
857 868 wiki_pages_map = {}
858 869 project.wiki.pages.each do |page|
859 870 # Skip pages without content
860 871 next if page.content.nil?
861 872 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
862 873 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
863 874 new_wiki_page.content = new_wiki_content
864 875 wiki.pages << new_wiki_page
865 876 wiki_pages_map[page.id] = new_wiki_page
866 877 end
867 878
868 879 self.wiki = wiki
869 880 wiki.save
870 881 # Reproduce page hierarchy
871 882 project.wiki.pages.each do |page|
872 883 if page.parent_id && wiki_pages_map[page.id]
873 884 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
874 885 wiki_pages_map[page.id].save
875 886 end
876 887 end
877 888 end
878 889 end
879 890
880 891 # Copies versions from +project+
881 892 def copy_versions(project)
882 893 project.versions.each do |version|
883 894 new_version = Version.new
884 895 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
885 896 self.versions << new_version
886 897 end
887 898 end
888 899
889 900 # Copies issue categories from +project+
890 901 def copy_issue_categories(project)
891 902 project.issue_categories.each do |issue_category|
892 903 new_issue_category = IssueCategory.new
893 904 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
894 905 self.issue_categories << new_issue_category
895 906 end
896 907 end
897 908
898 909 # Copies issues from +project+
899 910 def copy_issues(project)
900 911 # Stores the source issue id as a key and the copied issues as the
901 912 # value. Used to map the two together for issue relations.
902 913 issues_map = {}
903 914
904 915 # Store status and reopen locked/closed versions
905 916 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
906 917 version_statuses.each do |version, status|
907 918 version.update_attribute :status, 'open'
908 919 end
909 920
910 921 # Get issues sorted by root_id, lft so that parent issues
911 922 # get copied before their children
912 923 project.issues.reorder('root_id, lft').each do |issue|
913 924 new_issue = Issue.new
914 925 new_issue.copy_from(issue, :subtasks => false, :link => false)
915 926 new_issue.project = self
916 927 # Changing project resets the custom field values
917 928 # TODO: handle this in Issue#project=
918 929 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
919 930 # Reassign fixed_versions by name, since names are unique per project
920 931 if issue.fixed_version && issue.fixed_version.project == project
921 932 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
922 933 end
923 934 # Reassign version custom field values
924 935 new_issue.custom_field_values.each do |custom_value|
925 936 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
926 937 versions = Version.where(:id => custom_value.value).to_a
927 938 new_value = versions.map do |version|
928 939 if version.project == project
929 940 self.versions.detect {|v| v.name == version.name}.try(:id)
930 941 else
931 942 version.id
932 943 end
933 944 end
934 945 new_value.compact!
935 946 new_value = new_value.first unless custom_value.custom_field.multiple?
936 947 custom_value.value = new_value
937 948 end
938 949 end
939 950 # Reassign the category by name, since names are unique per project
940 951 if issue.category
941 952 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
942 953 end
943 954 # Parent issue
944 955 if issue.parent_id
945 956 if copied_parent = issues_map[issue.parent_id]
946 957 new_issue.parent_issue_id = copied_parent.id
947 958 end
948 959 end
949 960
950 961 self.issues << new_issue
951 962 if new_issue.new_record?
952 963 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
953 964 else
954 965 issues_map[issue.id] = new_issue unless new_issue.new_record?
955 966 end
956 967 end
957 968
958 969 # Restore locked/closed version statuses
959 970 version_statuses.each do |version, status|
960 971 version.update_attribute :status, status
961 972 end
962 973
963 974 # Relations after in case issues related each other
964 975 project.issues.each do |issue|
965 976 new_issue = issues_map[issue.id]
966 977 unless new_issue
967 978 # Issue was not copied
968 979 next
969 980 end
970 981
971 982 # Relations
972 983 issue.relations_from.each do |source_relation|
973 984 new_issue_relation = IssueRelation.new
974 985 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
975 986 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
976 987 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
977 988 new_issue_relation.issue_to = source_relation.issue_to
978 989 end
979 990 new_issue.relations_from << new_issue_relation
980 991 end
981 992
982 993 issue.relations_to.each do |source_relation|
983 994 new_issue_relation = IssueRelation.new
984 995 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
985 996 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
986 997 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
987 998 new_issue_relation.issue_from = source_relation.issue_from
988 999 end
989 1000 new_issue.relations_to << new_issue_relation
990 1001 end
991 1002 end
992 1003 end
993 1004
994 1005 # Copies members from +project+
995 1006 def copy_members(project)
996 1007 # Copy users first, then groups to handle members with inherited and given roles
997 1008 members_to_copy = []
998 1009 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
999 1010 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1000 1011
1001 1012 members_to_copy.each do |member|
1002 1013 new_member = Member.new
1003 1014 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1004 1015 # only copy non inherited roles
1005 1016 # inherited roles will be added when copying the group membership
1006 1017 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1007 1018 next if role_ids.empty?
1008 1019 new_member.role_ids = role_ids
1009 1020 new_member.project = self
1010 1021 self.members << new_member
1011 1022 end
1012 1023 end
1013 1024
1014 1025 # Copies queries from +project+
1015 1026 def copy_queries(project)
1016 1027 project.queries.each do |query|
1017 1028 new_query = IssueQuery.new
1018 1029 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1019 1030 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1020 1031 new_query.project = self
1021 1032 new_query.user_id = query.user_id
1022 1033 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1023 1034 self.queries << new_query
1024 1035 end
1025 1036 end
1026 1037
1027 1038 # Copies boards from +project+
1028 1039 def copy_boards(project)
1029 1040 project.boards.each do |board|
1030 1041 new_board = Board.new
1031 1042 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1032 1043 new_board.project = self
1033 1044 self.boards << new_board
1034 1045 end
1035 1046 end
1036 1047
1037 1048 def allowed_permissions
1038 1049 @allowed_permissions ||= begin
1039 1050 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1040 1051 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1041 1052 end
1042 1053 end
1043 1054
1044 1055 def allowed_actions
1045 1056 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1046 1057 end
1047 1058
1048 1059 # Archives subprojects recursively
1049 1060 def archive!
1050 1061 children.each do |subproject|
1051 1062 subproject.send :archive!
1052 1063 end
1053 1064 update_attribute :status, STATUS_ARCHIVED
1054 1065 end
1055 1066 end
@@ -1,284 +1,291
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Role < ActiveRecord::Base
19 19 # Custom coder for the permissions attribute that should be an
20 20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
21 21 # slow on some platforms (eg. mingw32).
22 22 class PermissionsAttributeCoder
23 23 def self.load(str)
24 24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
25 25 end
26 26
27 27 def self.dump(value)
28 28 YAML.dump(value)
29 29 end
30 30 end
31 31
32 32 # Built-in roles
33 33 BUILTIN_NON_MEMBER = 1
34 34 BUILTIN_ANONYMOUS = 2
35 35
36 36 ISSUES_VISIBILITY_OPTIONS = [
37 37 ['all', :label_issues_visibility_all],
38 38 ['default', :label_issues_visibility_public],
39 39 ['own', :label_issues_visibility_own]
40 40 ]
41 41
42 42 TIME_ENTRIES_VISIBILITY_OPTIONS = [
43 43 ['all', :label_time_entries_visibility_all],
44 44 ['own', :label_time_entries_visibility_own]
45 45 ]
46 46
47 47 USERS_VISIBILITY_OPTIONS = [
48 48 ['all', :label_users_visibility_all],
49 49 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
50 50 ]
51 51
52 52 scope :sorted, lambda { order(:builtin, :position) }
53 53 scope :givable, lambda { order(:position).where(:builtin => 0) }
54 54 scope :builtin, lambda { |*args|
55 55 compare = (args.first == true ? 'not' : '')
56 56 where("#{compare} builtin = 0")
57 57 }
58 58
59 59 before_destroy :check_deletable
60 60 has_many :workflow_rules, :dependent => :delete_all do
61 61 def copy(source_role)
62 62 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
63 63 end
64 64 end
65 65 has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
66 66
67 67 has_and_belongs_to_many :managed_roles, :class_name => 'Role',
68 68 :join_table => "#{table_name_prefix}roles_managed_roles#{table_name_suffix}",
69 69 :association_foreign_key => "managed_role_id"
70 70
71 71 has_many :member_roles, :dependent => :destroy
72 72 has_many :members, :through => :member_roles
73 73 acts_as_positioned :scope => :builtin
74 74
75 75 serialize :permissions, ::Role::PermissionsAttributeCoder
76 76 store :settings, :accessors => [:permissions_all_trackers, :permissions_tracker_ids]
77 77 attr_protected :builtin
78 78
79 79 validates_presence_of :name
80 80 validates_uniqueness_of :name
81 81 validates_length_of :name, :maximum => 30
82 82 validates_inclusion_of :issues_visibility,
83 83 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
84 84 :if => lambda {|role| role.respond_to?(:issues_visibility) && role.issues_visibility_changed?}
85 85 validates_inclusion_of :users_visibility,
86 86 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
87 87 :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?}
88 88 validates_inclusion_of :time_entries_visibility,
89 89 :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first),
90 90 :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?}
91 91
92 92 # Copies attributes from another role, arg can be an id or a Role
93 93 def copy_from(arg, options={})
94 94 return unless arg.present?
95 95 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
96 96 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
97 97 self.permissions = role.permissions.dup
98 98 self
99 99 end
100 100
101 101 def permissions=(perms)
102 102 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
103 103 write_attribute(:permissions, perms)
104 104 end
105 105
106 106 def add_permission!(*perms)
107 107 self.permissions = [] unless permissions.is_a?(Array)
108 108
109 109 permissions_will_change!
110 110 perms.each do |p|
111 111 p = p.to_sym
112 112 permissions << p unless permissions.include?(p)
113 113 end
114 114 save!
115 115 end
116 116
117 117 def remove_permission!(*perms)
118 118 return unless permissions.is_a?(Array)
119 119 permissions_will_change!
120 120 perms.each { |p| permissions.delete(p.to_sym) }
121 121 save!
122 122 end
123 123
124 124 # Returns true if the role has the given permission
125 125 def has_permission?(perm)
126 126 !permissions.nil? && permissions.include?(perm.to_sym)
127 127 end
128 128
129 129 def consider_workflow?
130 130 has_permission?(:add_issues) || has_permission?(:edit_issues)
131 131 end
132 132
133 133 def <=>(role)
134 134 if role
135 135 if builtin == role.builtin
136 136 position <=> role.position
137 137 else
138 138 builtin <=> role.builtin
139 139 end
140 140 else
141 141 -1
142 142 end
143 143 end
144 144
145 145 def to_s
146 146 name
147 147 end
148 148
149 149 def name
150 150 case builtin
151 151 when 1; l(:label_role_non_member, :default => read_attribute(:name))
152 152 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
153 153 else; read_attribute(:name)
154 154 end
155 155 end
156 156
157 157 # Return true if the role is a builtin role
158 158 def builtin?
159 159 self.builtin != 0
160 160 end
161 161
162 162 # Return true if the role is the anonymous role
163 163 def anonymous?
164 164 builtin == 2
165 165 end
166 166
167 167 # Return true if the role is a project member role
168 168 def member?
169 169 !self.builtin?
170 170 end
171 171
172 172 # Return true if role is allowed to do the specified action
173 173 # action can be:
174 174 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
175 175 # * a permission Symbol (eg. :edit_project)
176 176 def allowed_to?(action)
177 177 if action.is_a? Hash
178 178 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
179 179 else
180 180 allowed_permissions.include? action
181 181 end
182 182 end
183 183
184 184 # Return all the permissions that can be given to the role
185 185 def setable_permissions
186 186 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
187 187 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
188 188 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
189 189 setable_permissions
190 190 end
191 191
192 192 def permissions_tracker_ids(*args)
193 193 if args.any?
194 194 Array(permissions_tracker_ids[args.first.to_s]).map(&:to_i)
195 195 else
196 196 super || {}
197 197 end
198 198 end
199 199
200 200 def permissions_tracker_ids=(arg)
201 201 h = arg.to_hash
202 202 h.values.each {|v| v.reject!(&:blank?)}
203 203 super(h)
204 204 end
205 205
206 206 # Returns true if tracker_id belongs to the list of
207 207 # trackers for which permission is given
208 208 def permissions_tracker_ids?(permission, tracker_id)
209 209 permissions_tracker_ids(permission).include?(tracker_id)
210 210 end
211 211
212 212 def permissions_all_trackers
213 213 super || {}
214 214 end
215 215
216 216 def permissions_all_trackers=(arg)
217 217 super(arg.to_hash)
218 218 end
219 219
220 220 # Returns true if permission is given for all trackers
221 221 def permissions_all_trackers?(permission)
222 222 permissions_all_trackers[permission.to_s].to_s != '0'
223 223 end
224 224
225 # Returns true if permission is given for the tracker
226 # (explicitly or for all trackers)
227 def permissions_tracker?(permission, tracker)
228 permissions_all_trackers?(permission) ||
229 permissions_tracker_ids?(permission, tracker.try(:id))
230 end
231
225 232 # Sets the trackers that are allowed for a permission.
226 233 # tracker_ids can be an array of tracker ids or :all for
227 234 # no restrictions.
228 235 #
229 236 # Examples:
230 237 # role.set_permission_trackers :add_issues, [1, 3]
231 238 # role.set_permission_trackers :add_issues, :all
232 239 def set_permission_trackers(permission, tracker_ids)
233 240 h = {permission.to_s => (tracker_ids == :all ? '1' : '0')}
234 241 self.permissions_all_trackers = permissions_all_trackers.merge(h)
235 242
236 243 h = {permission.to_s => (tracker_ids == :all ? [] : tracker_ids)}
237 244 self.permissions_tracker_ids = permissions_tracker_ids.merge(h)
238 245
239 246 self
240 247 end
241 248
242 249 # Find all the roles that can be given to a project member
243 250 def self.find_all_givable
244 251 Role.givable.to_a
245 252 end
246 253
247 254 # Return the builtin 'non member' role. If the role doesn't exist,
248 255 # it will be created on the fly.
249 256 def self.non_member
250 257 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
251 258 end
252 259
253 260 # Return the builtin 'anonymous' role. If the role doesn't exist,
254 261 # it will be created on the fly.
255 262 def self.anonymous
256 263 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
257 264 end
258 265
259 266 private
260 267
261 268 def allowed_permissions
262 269 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
263 270 end
264 271
265 272 def allowed_actions
266 273 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
267 274 end
268 275
269 276 def check_deletable
270 277 raise "Cannot delete role" if members.any?
271 278 raise "Cannot delete builtin role" if builtin?
272 279 end
273 280
274 281 def self.find_or_create_system_role(builtin, name)
275 282 role = where(:builtin => builtin).first
276 283 if role.nil?
277 284 role = create(:name => name) do |r|
278 285 r.builtin = builtin
279 286 end
280 287 raise "Unable to create the #{name} role (#{role.errors.full_messages.join(',')})." if role.new_record?
281 288 end
282 289 role
283 290 end
284 291 end
@@ -1,2934 +1,2947
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :email_addresses, :user_preferences, :members, :member_roles, :roles,
22 22 :groups_users,
23 23 :trackers, :projects_trackers,
24 24 :enabled_modules,
25 25 :versions,
26 26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 27 :enumerations,
28 28 :issues, :journals, :journal_details,
29 29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 30 :time_entries
31 31
32 32 include Redmine::I18n
33 33
34 34 def setup
35 35 set_language_if_valid 'en'
36 36 end
37 37
38 38 def teardown
39 39 User.current = nil
40 40 end
41 41
42 42 def test_initialize
43 43 issue = Issue.new
44 44
45 45 assert_nil issue.project_id
46 46 assert_nil issue.tracker_id
47 47 assert_nil issue.status_id
48 48 assert_nil issue.author_id
49 49 assert_nil issue.assigned_to_id
50 50 assert_nil issue.category_id
51 51
52 52 assert_equal IssuePriority.default, issue.priority
53 53 end
54 54
55 55 def test_create
56 56 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
57 57 :status_id => 1, :priority => IssuePriority.all.first,
58 58 :subject => 'test_create',
59 59 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
60 60 assert issue.save
61 61 issue.reload
62 62 assert_equal 1.5, issue.estimated_hours
63 63 end
64 64
65 65 def test_create_minimal
66 66 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create')
67 67 assert issue.save
68 68 assert_equal issue.tracker.default_status, issue.status
69 69 assert issue.description.nil?
70 70 assert_nil issue.estimated_hours
71 71 end
72 72
73 73 def test_create_with_all_fields_disabled
74 74 tracker = Tracker.find(1)
75 75 tracker.core_fields = []
76 76 tracker.save!
77 77
78 78 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create_with_all_fields_disabled')
79 79 assert_save issue
80 80 end
81 81
82 82 def test_start_date_format_should_be_validated
83 83 set_language_if_valid 'en'
84 84 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
85 85 issue = Issue.new(:start_date => invalid_date)
86 86 assert !issue.valid?
87 87 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
88 88 end
89 89 end
90 90
91 91 def test_due_date_format_should_be_validated
92 92 set_language_if_valid 'en'
93 93 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
94 94 issue = Issue.new(:due_date => invalid_date)
95 95 assert !issue.valid?
96 96 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
97 97 end
98 98 end
99 99
100 100 def test_due_date_lesser_than_start_date_should_not_validate
101 101 set_language_if_valid 'en'
102 102 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
103 103 assert !issue.valid?
104 104 assert_include 'Due date must be greater than start date', issue.errors.full_messages
105 105 end
106 106
107 107 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
108 108 issue = Issue.generate(:start_date => '2013-06-04')
109 109 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
110 110 assert !issue.valid?
111 111 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
112 112 end
113 113
114 114 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
115 115 issue = Issue.generate!(:start_date => '2013-06-04')
116 116 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
117 117 issue.start_date = '2013-06-07'
118 118 assert !issue.valid?
119 119 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
120 120 end
121 121
122 122 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
123 123 issue = Issue.generate!(:start_date => '2013-06-04')
124 124 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
125 125 assert issue.valid?
126 126 end
127 127
128 128 def test_estimated_hours_should_be_validated
129 129 set_language_if_valid 'en'
130 130 ['-2'].each do |invalid|
131 131 issue = Issue.new(:estimated_hours => invalid)
132 132 assert !issue.valid?
133 133 assert_include 'Estimated time is invalid', issue.errors.full_messages
134 134 end
135 135 end
136 136
137 137 def test_create_with_required_custom_field
138 138 set_language_if_valid 'en'
139 139 field = IssueCustomField.find_by_name('Database')
140 140 field.update_attribute(:is_required, true)
141 141
142 142 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
143 143 :status_id => 1, :subject => 'test_create',
144 144 :description => 'IssueTest#test_create_with_required_custom_field')
145 145 assert issue.available_custom_fields.include?(field)
146 146 # No value for the custom field
147 147 assert !issue.save
148 148 assert_equal ["Database cannot be blank"], issue.errors.full_messages
149 149 # Blank value
150 150 issue.custom_field_values = { field.id => '' }
151 151 assert !issue.save
152 152 assert_equal ["Database cannot be blank"], issue.errors.full_messages
153 153 # Invalid value
154 154 issue.custom_field_values = { field.id => 'SQLServer' }
155 155 assert !issue.save
156 156 assert_equal ["Database is not included in the list"], issue.errors.full_messages
157 157 # Valid value
158 158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 159 assert issue.save
160 160 issue.reload
161 161 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
162 162 end
163 163
164 164 def test_create_with_group_assignment
165 165 with_settings :issue_group_assignment => '1' do
166 166 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
167 167 :subject => 'Group assignment',
168 168 :assigned_to_id => 11).save
169 169 issue = Issue.order('id DESC').first
170 170 assert_kind_of Group, issue.assigned_to
171 171 assert_equal Group.find(11), issue.assigned_to
172 172 end
173 173 end
174 174
175 175 def test_create_with_parent_issue_id
176 176 issue = Issue.new(:project_id => 1, :tracker_id => 1,
177 177 :author_id => 1, :subject => 'Group assignment',
178 178 :parent_issue_id => 1)
179 179 assert_save issue
180 180 assert_equal 1, issue.parent_issue_id
181 181 assert_equal Issue.find(1), issue.parent
182 182 end
183 183
184 184 def test_create_with_sharp_parent_issue_id
185 185 issue = Issue.new(:project_id => 1, :tracker_id => 1,
186 186 :author_id => 1, :subject => 'Group assignment',
187 187 :parent_issue_id => "#1")
188 188 assert_save issue
189 189 assert_equal 1, issue.parent_issue_id
190 190 assert_equal Issue.find(1), issue.parent
191 191 end
192 192
193 193 def test_create_with_invalid_parent_issue_id
194 194 set_language_if_valid 'en'
195 195 issue = Issue.new(:project_id => 1, :tracker_id => 1,
196 196 :author_id => 1, :subject => 'Group assignment',
197 197 :parent_issue_id => '01ABC')
198 198 assert !issue.save
199 199 assert_equal '01ABC', issue.parent_issue_id
200 200 assert_include 'Parent task is invalid', issue.errors.full_messages
201 201 end
202 202
203 203 def test_create_with_invalid_sharp_parent_issue_id
204 204 set_language_if_valid 'en'
205 205 issue = Issue.new(:project_id => 1, :tracker_id => 1,
206 206 :author_id => 1, :subject => 'Group assignment',
207 207 :parent_issue_id => '#01ABC')
208 208 assert !issue.save
209 209 assert_equal '#01ABC', issue.parent_issue_id
210 210 assert_include 'Parent task is invalid', issue.errors.full_messages
211 211 end
212 212
213 213 def assert_visibility_match(user, issues)
214 214 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
215 215 end
216 216
217 217 def test_visible_scope_for_anonymous
218 218 # Anonymous user should see issues of public projects only
219 219 issues = Issue.visible(User.anonymous).to_a
220 220 assert issues.any?
221 221 assert_nil issues.detect {|issue| !issue.project.is_public?}
222 222 assert_nil issues.detect {|issue| issue.is_private?}
223 223 assert_visibility_match User.anonymous, issues
224 224 end
225 225
226 226 def test_visible_scope_for_anonymous_without_view_issues_permissions
227 227 # Anonymous user should not see issues without permission
228 228 Role.anonymous.remove_permission!(:view_issues)
229 229 issues = Issue.visible(User.anonymous).to_a
230 230 assert issues.empty?
231 231 assert_visibility_match User.anonymous, issues
232 232 end
233 233
234 234 def test_visible_scope_for_anonymous_without_view_issues_permissions_and_membership
235 235 Role.anonymous.remove_permission!(:view_issues)
236 236 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [2])
237 237
238 238 issues = Issue.visible(User.anonymous).all
239 239 assert issues.any?
240 240 assert_equal [1], issues.map(&:project_id).uniq.sort
241 241 assert_visibility_match User.anonymous, issues
242 242 end
243 243
244 244 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
245 245 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
246 246 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
247 247 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
248 248 assert !issue.visible?(User.anonymous)
249 249 end
250 250
251 251 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
252 252 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
253 253 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
254 254 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
255 255 assert !issue.visible?(User.anonymous)
256 256 end
257 257
258 258 def test_visible_scope_for_non_member
259 259 user = User.find(9)
260 260 assert user.projects.empty?
261 261 # Non member user should see issues of public projects only
262 262 issues = Issue.visible(user).to_a
263 263 assert issues.any?
264 264 assert_nil issues.detect {|issue| !issue.project.is_public?}
265 265 assert_nil issues.detect {|issue| issue.is_private?}
266 266 assert_visibility_match user, issues
267 267 end
268 268
269 269 def test_visible_scope_for_non_member_with_own_issues_visibility
270 270 Role.non_member.update_attribute :issues_visibility, 'own'
271 271 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
272 272 user = User.find(9)
273 273
274 274 issues = Issue.visible(user).to_a
275 275 assert issues.any?
276 276 assert_nil issues.detect {|issue| issue.author != user}
277 277 assert_visibility_match user, issues
278 278 end
279 279
280 280 def test_visible_scope_for_non_member_without_view_issues_permissions
281 281 # Non member user should not see issues without permission
282 282 Role.non_member.remove_permission!(:view_issues)
283 283 user = User.find(9)
284 284 assert user.projects.empty?
285 285 issues = Issue.visible(user).to_a
286 286 assert issues.empty?
287 287 assert_visibility_match user, issues
288 288 end
289 289
290 290 def test_visible_scope_for_non_member_without_view_issues_permissions_and_membership
291 291 Role.non_member.remove_permission!(:view_issues)
292 292 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [2])
293 293 user = User.find(9)
294 294
295 295 issues = Issue.visible(user).all
296 296 assert issues.any?
297 297 assert_equal [1], issues.map(&:project_id).uniq.sort
298 298 assert_visibility_match user, issues
299 299 end
300 300
301 301 def test_visible_scope_for_member
302 302 user = User.find(9)
303 303 # User should see issues of projects for which user has view_issues permissions only
304 304 Role.non_member.remove_permission!(:view_issues)
305 305 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
306 306 issues = Issue.visible(user).to_a
307 307 assert issues.any?
308 308 assert_nil issues.detect {|issue| issue.project_id != 3}
309 309 assert_nil issues.detect {|issue| issue.is_private?}
310 310 assert_visibility_match user, issues
311 311 end
312 312
313 313 def test_visible_scope_for_member_without_view_issues_permission_and_non_member_role_having_the_permission
314 314 Role.non_member.add_permission!(:view_issues)
315 315 Role.find(1).remove_permission!(:view_issues)
316 316 user = User.find(2)
317 317
318 318 assert_equal 0, Issue.where(:project_id => 1).visible(user).count
319 319 assert_equal false, Issue.where(:project_id => 1).first.visible?(user)
320 320 end
321 321
322 322 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
323 323 user = User.find(8)
324 324 assert user.groups.any?
325 325 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
326 326 Role.non_member.remove_permission!(:view_issues)
327 327
328 328 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 3,
329 329 :status_id => 1, :priority => IssuePriority.all.first,
330 330 :subject => 'Assignment test',
331 331 :assigned_to => user.groups.first,
332 332 :is_private => true)
333 333
334 334 Role.find(2).update_attribute :issues_visibility, 'default'
335 335 issues = Issue.visible(User.find(8)).to_a
336 336 assert issues.any?
337 337 assert issues.include?(issue)
338 338
339 339 Role.find(2).update_attribute :issues_visibility, 'own'
340 340 issues = Issue.visible(User.find(8)).to_a
341 341 assert issues.any?
342 342 assert_include issue, issues
343 343 end
344 344
345 345 def test_visible_scope_for_member_with_limited_tracker_ids
346 346 role = Role.find(1)
347 347 role.set_permission_trackers :view_issues, [2]
348 348 role.save!
349 349 user = User.find(2)
350 350
351 351 issues = Issue.where(:project_id => 1).visible(user).to_a
352 352 assert issues.any?
353 353 assert_equal [2], issues.map(&:tracker_id).uniq
354 354
355 355 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
356 356 end
357 357
358 358 def test_visible_scope_should_consider_tracker_ids_on_each_project
359 359 user = User.generate!
360 360
361 361 project1 = Project.generate!
362 362 role1 = Role.generate!
363 363 role1.add_permission! :view_issues
364 364 role1.set_permission_trackers :view_issues, :all
365 365 role1.save!
366 366 User.add_to_project(user, project1, role1)
367 367
368 368 project2 = Project.generate!
369 369 role2 = Role.generate!
370 370 role2.add_permission! :view_issues
371 371 role2.set_permission_trackers :view_issues, [2]
372 372 role2.save!
373 373 User.add_to_project(user, project2, role2)
374 374
375 375 visible_issues = [
376 376 Issue.generate!(:project => project1, :tracker_id => 1),
377 377 Issue.generate!(:project => project1, :tracker_id => 2),
378 378 Issue.generate!(:project => project2, :tracker_id => 2)
379 379 ]
380 380 hidden_issue = Issue.generate!(:project => project2, :tracker_id => 1)
381 381
382 382 issues = Issue.where(:project_id => [project1.id, project2.id]).visible(user)
383 383 assert_equal visible_issues.map(&:id), issues.ids.sort
384 384
385 385 assert visible_issues.all? {|issue| issue.visible?(user)}
386 386 assert !hidden_issue.visible?(user)
387 387 end
388 388
389 389 def test_visible_scope_should_not_consider_roles_without_view_issues_permission
390 390 user = User.generate!
391 391 role1 = Role.generate!
392 392 role1.remove_permission! :view_issues
393 393 role1.set_permission_trackers :view_issues, :all
394 394 role1.save!
395 395 role2 = Role.generate!
396 396 role2.add_permission! :view_issues
397 397 role2.set_permission_trackers :view_issues, [2]
398 398 role2.save!
399 399 User.add_to_project(user, Project.find(1), [role1, role2])
400 400
401 401 issues = Issue.where(:project_id => 1).visible(user).to_a
402 402 assert issues.any?
403 403 assert_equal [2], issues.map(&:tracker_id).uniq
404 404
405 405 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
406 406 end
407 407
408 408 def test_visible_scope_for_admin
409 409 user = User.find(1)
410 410 user.members.each(&:destroy)
411 411 assert user.projects.empty?
412 412 issues = Issue.visible(user).to_a
413 413 assert issues.any?
414 414 # Admin should see issues on private projects that admin does not belong to
415 415 assert issues.detect {|issue| !issue.project.is_public?}
416 416 # Admin should see private issues of other users
417 417 assert issues.detect {|issue| issue.is_private? && issue.author != user}
418 418 assert_visibility_match user, issues
419 419 end
420 420
421 421 def test_visible_scope_with_project
422 422 project = Project.find(1)
423 423 issues = Issue.visible(User.find(2), :project => project).to_a
424 424 projects = issues.collect(&:project).uniq
425 425 assert_equal 1, projects.size
426 426 assert_equal project, projects.first
427 427 end
428 428
429 429 def test_visible_scope_with_project_and_subprojects
430 430 project = Project.find(1)
431 431 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).to_a
432 432 projects = issues.collect(&:project).uniq
433 433 assert projects.size > 1
434 434 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
435 435 end
436 436
437 437 def test_visible_and_nested_set_scopes
438 438 user = User.generate!
439 439 parent = Issue.generate!(:assigned_to => user)
440 440 assert parent.visible?(user)
441 441 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
442 442 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
443 443 parent.reload
444 444 child1.reload
445 445 child2.reload
446 446 assert child1.visible?(user)
447 447 assert child2.visible?(user)
448 448 assert_equal 2, parent.descendants.count
449 449 assert_equal 2, parent.descendants.visible(user).count
450 450 # awesome_nested_set 2-1-stable branch has regression.
451 451 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
452 452 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
453 453 assert_equal 2, parent.descendants.collect{|i| i}.size
454 454 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
455 455 end
456 456
457 457 def test_visible_scope_with_unsaved_user_should_not_raise_an_error
458 458 user = User.new
459 459 assert_nothing_raised do
460 460 Issue.visible(user).to_a
461 461 end
462 462 end
463 463
464 464 def test_open_scope
465 465 issues = Issue.open.to_a
466 466 assert_nil issues.detect(&:closed?)
467 467 end
468 468
469 469 def test_open_scope_with_arg
470 470 issues = Issue.open(false).to_a
471 471 assert_equal issues, issues.select(&:closed?)
472 472 end
473 473
474 474 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
475 475 version = Version.find(2)
476 476 assert version.fixed_issues.any?
477 477 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
478 478 end
479 479
480 480 def test_fixed_version_scope_with_empty_array_should_return_no_result
481 481 assert_equal 0, Issue.fixed_version([]).count
482 482 end
483 483
484 484 def test_assigned_to_scope_should_return_issues_assigned_to_the_user
485 485 user = User.generate!
486 486 issue = Issue.generate!
487 487 Issue.where(:id => issue.id).update_all :assigned_to_id => user.id
488 488 assert_equal [issue], Issue.assigned_to(user).to_a
489 489 end
490 490
491 491 def test_assigned_to_scope_should_return_issues_assigned_to_the_user_groups
492 492 group = Group.generate!
493 493 user = User.generate!
494 494 group.users << user
495 495 issue = Issue.generate!
496 496 Issue.where(:id => issue.id).update_all :assigned_to_id => group.id
497 497 assert_equal [issue], Issue.assigned_to(user).to_a
498 498 end
499 499
500 500 def test_errors_full_messages_should_include_custom_fields_errors
501 501 field = IssueCustomField.find_by_name('Database')
502 502
503 503 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
504 504 :status_id => 1, :subject => 'test_create',
505 505 :description => 'IssueTest#test_create_with_required_custom_field')
506 506 assert issue.available_custom_fields.include?(field)
507 507 # Invalid value
508 508 issue.custom_field_values = { field.id => 'SQLServer' }
509 509
510 510 assert !issue.valid?
511 511 assert_equal 1, issue.errors.full_messages.size
512 512 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
513 513 issue.errors.full_messages.first
514 514 end
515 515
516 516 def test_update_issue_with_required_custom_field
517 517 field = IssueCustomField.find_by_name('Database')
518 518 field.update_attribute(:is_required, true)
519 519
520 520 issue = Issue.find(1)
521 521 assert_nil issue.custom_value_for(field)
522 522 assert issue.available_custom_fields.include?(field)
523 523 # No change to custom values, issue can be saved
524 524 assert issue.save
525 525 # Blank value
526 526 issue.custom_field_values = { field.id => '' }
527 527 assert !issue.save
528 528 # Valid value
529 529 issue.custom_field_values = { field.id => 'PostgreSQL' }
530 530 assert issue.save
531 531 issue.reload
532 532 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
533 533 end
534 534
535 535 def test_should_not_update_attributes_if_custom_fields_validation_fails
536 536 issue = Issue.find(1)
537 537 field = IssueCustomField.find_by_name('Database')
538 538 assert issue.available_custom_fields.include?(field)
539 539
540 540 issue.custom_field_values = { field.id => 'Invalid' }
541 541 issue.subject = 'Should be not be saved'
542 542 assert !issue.save
543 543
544 544 issue.reload
545 545 assert_equal "Cannot print recipes", issue.subject
546 546 end
547 547
548 548 def test_should_not_recreate_custom_values_objects_on_update
549 549 field = IssueCustomField.find_by_name('Database')
550 550
551 551 issue = Issue.find(1)
552 552 issue.custom_field_values = { field.id => 'PostgreSQL' }
553 553 assert issue.save
554 554 custom_value = issue.custom_value_for(field)
555 555 issue.reload
556 556 issue.custom_field_values = { field.id => 'MySQL' }
557 557 assert issue.save
558 558 issue.reload
559 559 assert_equal custom_value.id, issue.custom_value_for(field).id
560 560 end
561 561
562 562 def test_setting_project_should_set_version_to_default_version
563 563 version = Version.generate!(:project_id => 1)
564 564 Project.find(1).update_attribute(:default_version_id, version.id)
565 565
566 566 issue = Issue.new(:project_id => 1)
567 567 assert_equal version, issue.fixed_version
568 568 end
569 569
570 570 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
571 571 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
572 572 :status_id => 1, :subject => 'Test',
573 573 :custom_field_values => {'2' => 'Test'})
574 574 assert !Tracker.find(2).custom_field_ids.include?(2)
575 575
576 576 issue = Issue.find(issue.id)
577 577 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
578 578
579 579 issue = Issue.find(issue.id)
580 580 custom_value = issue.custom_value_for(2)
581 581 assert_not_nil custom_value
582 582 assert_equal 'Test', custom_value.value
583 583 end
584 584
585 585 def test_assigning_tracker_id_should_reload_custom_fields_values
586 586 issue = Issue.new(:project => Project.find(1))
587 587 assert issue.custom_field_values.empty?
588 588 issue.tracker_id = 1
589 589 assert issue.custom_field_values.any?
590 590 end
591 591
592 592 def test_assigning_attributes_should_assign_project_and_tracker_first
593 593 seq = sequence('seq')
594 594 issue = Issue.new
595 595 issue.expects(:project_id=).in_sequence(seq)
596 596 issue.expects(:tracker_id=).in_sequence(seq)
597 597 issue.expects(:subject=).in_sequence(seq)
598 598 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
599 599 end
600 600
601 601 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
602 602 attributes = ActiveSupport::OrderedHash.new
603 603 attributes['custom_field_values'] = { '1' => 'MySQL' }
604 604 attributes['tracker_id'] = '1'
605 605 issue = Issue.new(:project => Project.find(1))
606 606 issue.attributes = attributes
607 607 assert_equal 'MySQL', issue.custom_field_value(1)
608 608 end
609 609
610 610 def test_changing_tracker_should_clear_disabled_core_fields
611 611 tracker = Tracker.find(2)
612 612 tracker.core_fields = tracker.core_fields - %w(due_date)
613 613 tracker.save!
614 614
615 615 issue = Issue.generate!(:tracker_id => 1, :start_date => Date.today, :due_date => Date.today)
616 616 issue.save!
617 617
618 618 issue.tracker_id = 2
619 619 issue.save!
620 620 assert_not_nil issue.start_date
621 621 assert_nil issue.due_date
622 622 end
623 623
624 624 def test_changing_tracker_should_not_add_cleared_fields_to_journal
625 625 tracker = Tracker.find(2)
626 626 tracker.core_fields = tracker.core_fields - %w(due_date)
627 627 tracker.save!
628 628
629 629 issue = Issue.generate!(:tracker_id => 1, :due_date => Date.today)
630 630 issue.save!
631 631
632 632 assert_difference 'Journal.count' do
633 633 issue.init_journal User.find(1)
634 634 issue.tracker_id = 2
635 635 issue.save!
636 636 assert_nil issue.due_date
637 637 end
638 638 journal = Journal.order('id DESC').first
639 639 assert_equal 1, journal.details.count
640 640 end
641 641
642 642 def test_reload_should_reload_custom_field_values
643 643 issue = Issue.generate!
644 644 issue.custom_field_values = {'2' => 'Foo'}
645 645 issue.save!
646 646
647 647 issue = Issue.order('id desc').first
648 648 assert_equal 'Foo', issue.custom_field_value(2)
649 649
650 650 issue.custom_field_values = {'2' => 'Bar'}
651 651 assert_equal 'Bar', issue.custom_field_value(2)
652 652
653 653 issue.reload
654 654 assert_equal 'Foo', issue.custom_field_value(2)
655 655 end
656 656
657 657 def test_should_update_issue_with_disabled_tracker
658 658 p = Project.find(1)
659 659 issue = Issue.find(1)
660 660
661 661 p.trackers.delete(issue.tracker)
662 662 assert !p.trackers.include?(issue.tracker)
663 663
664 664 issue.reload
665 665 issue.subject = 'New subject'
666 666 assert issue.save
667 667 end
668 668
669 669 def test_should_not_set_a_disabled_tracker
670 670 p = Project.find(1)
671 671 p.trackers.delete(Tracker.find(2))
672 672
673 673 issue = Issue.find(1)
674 674 issue.tracker_id = 2
675 675 issue.subject = 'New subject'
676 676 assert !issue.save
677 677 assert_not_equal [], issue.errors[:tracker_id]
678 678 end
679 679
680 680 def test_category_based_assignment
681 681 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
682 682 :status_id => 1, :priority => IssuePriority.all.first,
683 683 :subject => 'Assignment test',
684 684 :description => 'Assignment test', :category_id => 1)
685 685 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
686 686 end
687 687
688 688 def test_new_statuses_allowed_to
689 689 WorkflowTransition.delete_all
690 690 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
691 691 :old_status_id => 1, :new_status_id => 2,
692 692 :author => false, :assignee => false)
693 693 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
694 694 :old_status_id => 1, :new_status_id => 3,
695 695 :author => true, :assignee => false)
696 696 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
697 697 :old_status_id => 1, :new_status_id => 4,
698 698 :author => false, :assignee => true)
699 699 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
700 700 :old_status_id => 1, :new_status_id => 5,
701 701 :author => true, :assignee => true)
702 702 status = IssueStatus.find(1)
703 703 role = Role.find(1)
704 704 tracker = Tracker.find(1)
705 705 user = User.find(2)
706 706
707 707 issue = Issue.generate!(:tracker => tracker, :status => status,
708 708 :project_id => 1, :author_id => 1)
709 709 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
710 710
711 711 issue = Issue.generate!(:tracker => tracker, :status => status,
712 712 :project_id => 1, :author => user)
713 713 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
714 714
715 715 issue = Issue.generate!(:tracker => tracker, :status => status,
716 716 :project_id => 1, :author_id => 1,
717 717 :assigned_to => user)
718 718 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
719 719
720 720 issue = Issue.generate!(:tracker => tracker, :status => status,
721 721 :project_id => 1, :author => user,
722 722 :assigned_to => user)
723 723 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
724 724
725 725 group = Group.generate!
726 726 group.users << user
727 727 issue = Issue.generate!(:tracker => tracker, :status => status,
728 728 :project_id => 1, :author => user,
729 729 :assigned_to => group)
730 730 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
731 731 end
732 732
733 733 def test_new_statuses_allowed_to_should_consider_group_assignment
734 734 WorkflowTransition.delete_all
735 735 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
736 736 :old_status_id => 1, :new_status_id => 4,
737 737 :author => false, :assignee => true)
738 738 user = User.find(2)
739 739 group = Group.generate!
740 740 group.users << user
741 741
742 742 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
743 743 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
744 744 end
745 745
746 746 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
747 747 admin = User.find(1)
748 748 issue = Issue.find(1)
749 749 assert !admin.member_of?(issue.project)
750 750 expected_statuses = [issue.status] +
751 751 WorkflowTransition.where(:old_status_id => issue.status_id).
752 752 map(&:new_status).uniq.sort
753 753 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
754 754 end
755 755
756 756 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
757 757 issue = Issue.find(1).copy
758 758 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
759 759
760 760 issue = Issue.find(2).copy
761 761 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
762 762 end
763 763
764 764 def test_safe_attributes_names_should_not_include_disabled_field
765 765 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
766 766
767 767 issue = Issue.new(:tracker => tracker)
768 768 assert_include 'tracker_id', issue.safe_attribute_names
769 769 assert_include 'status_id', issue.safe_attribute_names
770 770 assert_include 'subject', issue.safe_attribute_names
771 771 assert_include 'description', issue.safe_attribute_names
772 772 assert_include 'custom_field_values', issue.safe_attribute_names
773 773 assert_include 'custom_fields', issue.safe_attribute_names
774 774 assert_include 'lock_version', issue.safe_attribute_names
775 775
776 776 tracker.core_fields.each do |field|
777 777 assert_include field, issue.safe_attribute_names
778 778 end
779 779
780 780 tracker.disabled_core_fields.each do |field|
781 781 assert_not_include field, issue.safe_attribute_names
782 782 end
783 783 end
784 784
785 785 def test_safe_attributes_should_ignore_disabled_fields
786 786 tracker = Tracker.find(1)
787 787 tracker.core_fields = %w(assigned_to_id due_date)
788 788 tracker.save!
789 789
790 790 issue = Issue.new(:tracker => tracker)
791 791 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
792 792 assert_nil issue.start_date
793 793 assert_equal Date.parse('2012-07-14'), issue.due_date
794 794 end
795 795
796 796 def test_safe_attributes_should_accept_target_tracker_enabled_fields
797 797 source = Tracker.find(1)
798 798 source.core_fields = []
799 799 source.save!
800 800 target = Tracker.find(2)
801 801 target.core_fields = %w(assigned_to_id due_date)
802 802 target.save!
803 803 user = User.find(2)
804 804
805 805 issue = Issue.new(:project => Project.find(1), :tracker => source)
806 806 issue.send :safe_attributes=, {'tracker_id' => 2, 'due_date' => '2012-07-14'}, user
807 807 assert_equal target, issue.tracker
808 808 assert_equal Date.parse('2012-07-14'), issue.due_date
809 809 end
810 810
811 811 def test_safe_attributes_should_not_include_readonly_fields
812 812 WorkflowPermission.delete_all
813 813 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
814 814 :role_id => 1, :field_name => 'due_date',
815 815 :rule => 'readonly')
816 816 user = User.find(2)
817 817
818 818 issue = Issue.new(:project_id => 1, :tracker_id => 1)
819 819 assert_equal %w(due_date), issue.read_only_attribute_names(user)
820 820 assert_not_include 'due_date', issue.safe_attribute_names(user)
821 821
822 822 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
823 823 assert_equal Date.parse('2012-07-14'), issue.start_date
824 824 assert_nil issue.due_date
825 825 end
826 826
827 827 def test_safe_attributes_should_not_include_readonly_custom_fields
828 828 cf1 = IssueCustomField.create!(:name => 'Writable field',
829 829 :field_format => 'string',
830 830 :is_for_all => true, :tracker_ids => [1])
831 831 cf2 = IssueCustomField.create!(:name => 'Readonly field',
832 832 :field_format => 'string',
833 833 :is_for_all => true, :tracker_ids => [1])
834 834 WorkflowPermission.delete_all
835 835 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
836 836 :role_id => 1, :field_name => cf2.id.to_s,
837 837 :rule => 'readonly')
838 838 user = User.find(2)
839 839 issue = Issue.new(:project_id => 1, :tracker_id => 1)
840 840 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
841 841 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
842 842
843 843 issue.send :safe_attributes=, {'custom_field_values' => {
844 844 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
845 845 }}, user
846 846 assert_equal 'value1', issue.custom_field_value(cf1)
847 847 assert_nil issue.custom_field_value(cf2)
848 848
849 849 issue.send :safe_attributes=, {'custom_fields' => [
850 850 {'id' => cf1.id.to_s, 'value' => 'valuea'},
851 851 {'id' => cf2.id.to_s, 'value' => 'valueb'}
852 852 ]}, user
853 853 assert_equal 'valuea', issue.custom_field_value(cf1)
854 854 assert_nil issue.custom_field_value(cf2)
855 855 end
856 856
857 857 def test_safe_attributes_should_ignore_unassignable_assignee
858 858 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
859 859 :status_id => 1, :priority => IssuePriority.all.first,
860 860 :subject => 'test_create')
861 861 assert issue.valid?
862 862
863 863 # locked user, not allowed
864 864 issue.safe_attributes=({'assigned_to_id' => '5'})
865 865 assert_nil issue.assigned_to_id
866 866 # no member
867 867 issue.safe_attributes=({'assigned_to_id' => '1'})
868 868 assert_nil issue.assigned_to_id
869 869 # user 2 is ok
870 870 issue.safe_attributes=({'assigned_to_id' => '2'})
871 871 assert_equal 2, issue.assigned_to_id
872 872 assert issue.save
873 873
874 874 issue.reload
875 875 assert_equal 2, issue.assigned_to_id
876 876 issue.safe_attributes=({'assigned_to_id' => '5'})
877 877 assert_equal 2, issue.assigned_to_id
878 878 issue.safe_attributes=({'assigned_to_id' => '1'})
879 879 assert_equal 2, issue.assigned_to_id
880 880 # user 3 is also ok
881 881 issue.safe_attributes=({'assigned_to_id' => '3'})
882 882 assert_equal 3, issue.assigned_to_id
883 883 assert issue.save
884 884
885 885 # removal of assignee
886 886 issue.safe_attributes=({'assigned_to_id' => ''})
887 887 assert_nil issue.assigned_to_id
888 888 assert issue.save
889 889 end
890 890
891 891 def test_editable_custom_field_values_should_return_non_readonly_custom_values
892 892 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
893 893 :is_for_all => true, :tracker_ids => [1, 2])
894 894 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
895 895 :is_for_all => true, :tracker_ids => [1, 2])
896 896 WorkflowPermission.delete_all
897 897 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
898 898 :field_name => cf2.id.to_s, :rule => 'readonly')
899 899 user = User.find(2)
900 900
901 901 issue = Issue.new(:project_id => 1, :tracker_id => 1)
902 902 values = issue.editable_custom_field_values(user)
903 903 assert values.detect {|value| value.custom_field == cf1}
904 904 assert_nil values.detect {|value| value.custom_field == cf2}
905 905
906 906 issue.tracker_id = 2
907 907 values = issue.editable_custom_field_values(user)
908 908 assert values.detect {|value| value.custom_field == cf1}
909 909 assert values.detect {|value| value.custom_field == cf2}
910 910 end
911 911
912 912 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
913 913 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
914 914 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
915 915 user = User.find(2)
916 916 issue = Issue.new(:project_id => 1, :tracker_id => 1)
917 917
918 918 assert_include enabled_cf, issue.editable_custom_fields(user)
919 919 assert_not_include disabled_cf, issue.editable_custom_fields(user)
920 920 end
921 921
922 922 def test_safe_attributes_should_accept_target_tracker_writable_fields
923 923 WorkflowPermission.delete_all
924 924 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
925 925 :role_id => 1, :field_name => 'due_date',
926 926 :rule => 'readonly')
927 927 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
928 928 :role_id => 1, :field_name => 'start_date',
929 929 :rule => 'readonly')
930 930 user = User.find(2)
931 931
932 932 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
933 933
934 934 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
935 935 'due_date' => '2012-07-14'}, user
936 936 assert_equal Date.parse('2012-07-12'), issue.start_date
937 937 assert_nil issue.due_date
938 938
939 939 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
940 940 'due_date' => '2012-07-16',
941 941 'tracker_id' => 2}, user
942 942 assert_equal Date.parse('2012-07-12'), issue.start_date
943 943 assert_equal Date.parse('2012-07-16'), issue.due_date
944 944 end
945 945
946 946 def test_safe_attributes_should_accept_target_status_writable_fields
947 947 WorkflowPermission.delete_all
948 948 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
949 949 :role_id => 1, :field_name => 'due_date',
950 950 :rule => 'readonly')
951 951 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
952 952 :role_id => 1, :field_name => 'start_date',
953 953 :rule => 'readonly')
954 954 user = User.find(2)
955 955
956 956 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
957 957
958 958 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
959 959 'due_date' => '2012-07-14'},
960 960 user
961 961 assert_equal Date.parse('2012-07-12'), issue.start_date
962 962 assert_nil issue.due_date
963 963
964 964 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
965 965 'due_date' => '2012-07-16',
966 966 'status_id' => 2},
967 967 user
968 968 assert_equal Date.parse('2012-07-12'), issue.start_date
969 969 assert_equal Date.parse('2012-07-16'), issue.due_date
970 970 end
971 971
972 972 def test_required_attributes_should_be_validated
973 973 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
974 974 :is_for_all => true, :tracker_ids => [1, 2])
975 975
976 976 WorkflowPermission.delete_all
977 977 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
978 978 :role_id => 1, :field_name => 'due_date',
979 979 :rule => 'required')
980 980 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
981 981 :role_id => 1, :field_name => 'category_id',
982 982 :rule => 'required')
983 983 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
984 984 :role_id => 1, :field_name => cf.id.to_s,
985 985 :rule => 'required')
986 986
987 987 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
988 988 :role_id => 1, :field_name => 'start_date',
989 989 :rule => 'required')
990 990 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
991 991 :role_id => 1, :field_name => cf.id.to_s,
992 992 :rule => 'required')
993 993 user = User.find(2)
994 994
995 995 issue = Issue.new(:project_id => 1, :tracker_id => 1,
996 996 :status_id => 1, :subject => 'Required fields',
997 997 :author => user)
998 998 assert_equal [cf.id.to_s, "category_id", "due_date"],
999 999 issue.required_attribute_names(user).sort
1000 1000 assert !issue.save, "Issue was saved"
1001 1001 assert_equal ["Category cannot be blank", "Due date cannot be blank", "Foo cannot be blank"],
1002 1002 issue.errors.full_messages.sort
1003 1003
1004 1004 issue.tracker_id = 2
1005 1005 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
1006 1006 assert !issue.save, "Issue was saved"
1007 1007 assert_equal ["Foo cannot be blank", "Start date cannot be blank"],
1008 1008 issue.errors.full_messages.sort
1009 1009
1010 1010 issue.start_date = Date.today
1011 1011 issue.custom_field_values = {cf.id.to_s => 'bar'}
1012 1012 assert issue.save
1013 1013 end
1014 1014
1015 1015 def test_required_attribute_that_is_disabled_for_the_tracker_should_not_be_required
1016 1016 WorkflowPermission.delete_all
1017 1017 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1018 1018 :role_id => 1, :field_name => 'start_date',
1019 1019 :rule => 'required')
1020 1020 user = User.find(2)
1021 1021
1022 1022 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1023 1023 :subject => 'Required fields', :author => user)
1024 1024 assert !issue.save
1025 1025 assert_include "Start date cannot be blank", issue.errors.full_messages
1026 1026
1027 1027 tracker = Tracker.find(1)
1028 1028 tracker.core_fields -= %w(start_date)
1029 1029 tracker.save!
1030 1030 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1031 1031 :subject => 'Required fields', :author => user)
1032 1032 assert issue.save
1033 1033 end
1034 1034
1035 1035 def test_category_should_not_be_required_if_project_has_no_categories
1036 1036 Project.find(1).issue_categories.delete_all
1037 1037 WorkflowPermission.delete_all
1038 1038 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1039 1039 :role_id => 1, :field_name => 'category_id',:rule => 'required')
1040 1040 user = User.find(2)
1041 1041
1042 1042 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1043 1043 :subject => 'Required fields', :author => user)
1044 1044 assert_save issue
1045 1045 end
1046 1046
1047 1047 def test_fixed_version_should_not_be_required_no_assignable_versions
1048 1048 Version.delete_all
1049 1049 WorkflowPermission.delete_all
1050 1050 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1051 1051 :role_id => 1, :field_name => 'fixed_version_id',:rule => 'required')
1052 1052 user = User.find(2)
1053 1053
1054 1054 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1055 1055 :subject => 'Required fields', :author => user)
1056 1056 assert_save issue
1057 1057 end
1058 1058
1059 1059 def test_required_custom_field_that_is_not_visible_for_the_user_should_not_be_required
1060 1060 CustomField.delete_all
1061 1061 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1062 1062 user = User.generate!
1063 1063 User.add_to_project(user, Project.find(1), Role.find(2))
1064 1064
1065 1065 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1066 1066 :subject => 'Required fields', :author => user)
1067 1067 assert_save issue
1068 1068 end
1069 1069
1070 1070 def test_required_custom_field_that_is_visible_for_the_user_should_be_required
1071 1071 CustomField.delete_all
1072 1072 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1073 1073 user = User.generate!
1074 1074 User.add_to_project(user, Project.find(1), Role.find(1))
1075 1075
1076 1076 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1077 1077 :subject => 'Required fields', :author => user)
1078 1078 assert !issue.save
1079 1079 assert_include "#{field.name} cannot be blank", issue.errors.full_messages
1080 1080 end
1081 1081
1082 1082 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
1083 1083 WorkflowPermission.delete_all
1084 1084 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1085 1085 :role_id => 1, :field_name => 'due_date',
1086 1086 :rule => 'required')
1087 1087 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1088 1088 :role_id => 1, :field_name => 'start_date',
1089 1089 :rule => 'required')
1090 1090 user = User.find(2)
1091 1091 member = Member.find(1)
1092 1092 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1093 1093
1094 1094 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
1095 1095
1096 1096 member.role_ids = [1, 2]
1097 1097 member.save!
1098 1098 assert_equal [], issue.required_attribute_names(user.reload)
1099 1099
1100 1100 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1101 1101 :role_id => 2, :field_name => 'due_date',
1102 1102 :rule => 'required')
1103 1103 assert_equal %w(due_date), issue.required_attribute_names(user)
1104 1104
1105 1105 member.role_ids = [1, 2, 3]
1106 1106 member.save!
1107 1107 assert_equal [], issue.required_attribute_names(user.reload)
1108 1108
1109 1109 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1110 1110 :role_id => 3, :field_name => 'due_date',
1111 1111 :rule => 'readonly')
1112 1112 # required + readonly => required
1113 1113 assert_equal %w(due_date), issue.required_attribute_names(user)
1114 1114 end
1115 1115
1116 1116 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
1117 1117 WorkflowPermission.delete_all
1118 1118 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1119 1119 :role_id => 1, :field_name => 'due_date',
1120 1120 :rule => 'readonly')
1121 1121 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1122 1122 :role_id => 1, :field_name => 'start_date',
1123 1123 :rule => 'readonly')
1124 1124 user = User.find(2)
1125 1125 member = Member.find(1)
1126 1126 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1127 1127
1128 1128 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
1129 1129
1130 1130 member.role_ids = [1, 2]
1131 1131 member.save!
1132 1132 assert_equal [], issue.read_only_attribute_names(user.reload)
1133 1133
1134 1134 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1135 1135 :role_id => 2, :field_name => 'due_date',
1136 1136 :rule => 'readonly')
1137 1137 assert_equal %w(due_date), issue.read_only_attribute_names(user)
1138 1138 end
1139 1139
1140 1140 # A field that is not visible by role 2 and readonly by role 1 should be readonly for user with role 1 and 2
1141 1141 def test_read_only_attribute_names_should_include_custom_fields_that_combine_readonly_and_not_visible_for_roles
1142 1142 field = IssueCustomField.generate!(
1143 1143 :is_for_all => true, :trackers => Tracker.all, :visible => false, :role_ids => [1]
1144 1144 )
1145 1145 WorkflowPermission.delete_all
1146 1146 WorkflowPermission.create!(
1147 1147 :old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => field.id, :rule => 'readonly'
1148 1148 )
1149 1149 user = User.generate!
1150 1150 project = Project.find(1)
1151 1151 User.add_to_project(user, project, Role.where(:id => [1, 2]))
1152 1152
1153 1153 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1154 1154 assert_equal [field.id.to_s], issue.read_only_attribute_names(user)
1155 1155 end
1156 1156
1157 1157 def test_workflow_rules_should_ignore_roles_without_issue_permissions
1158 1158 role = Role.generate! :permissions => [:view_issues, :edit_issues]
1159 1159 ignored_role = Role.generate! :permissions => [:view_issues]
1160 1160
1161 1161 WorkflowPermission.delete_all
1162 1162 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1163 1163 :role => role, :field_name => 'due_date',
1164 1164 :rule => 'required')
1165 1165 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1166 1166 :role => role, :field_name => 'start_date',
1167 1167 :rule => 'readonly')
1168 1168 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1169 1169 :role => role, :field_name => 'done_ratio',
1170 1170 :rule => 'readonly')
1171 1171 user = User.generate!
1172 1172 User.add_to_project user, Project.find(1), [role, ignored_role]
1173 1173
1174 1174 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1175 1175
1176 1176 assert_equal %w(due_date), issue.required_attribute_names(user)
1177 1177 assert_equal %w(done_ratio start_date), issue.read_only_attribute_names(user).sort
1178 1178 end
1179 1179
1180 1180 def test_workflow_rules_should_work_for_member_with_duplicate_role
1181 1181 WorkflowPermission.delete_all
1182 1182 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1183 1183 :role_id => 1, :field_name => 'due_date',
1184 1184 :rule => 'required')
1185 1185 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1186 1186 :role_id => 1, :field_name => 'start_date',
1187 1187 :rule => 'readonly')
1188 1188
1189 1189 user = User.generate!
1190 1190 m = Member.new(:user_id => user.id, :project_id => 1)
1191 1191 m.member_roles.build(:role_id => 1)
1192 1192 m.member_roles.build(:role_id => 1)
1193 1193 m.save!
1194 1194
1195 1195 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1196 1196
1197 1197 assert_equal %w(due_date), issue.required_attribute_names(user)
1198 1198 assert_equal %w(start_date), issue.read_only_attribute_names(user)
1199 1199 end
1200 1200
1201 1201 def test_copy
1202 1202 issue = Issue.new.copy_from(1)
1203 1203 assert issue.copy?
1204 1204 assert issue.save
1205 1205 issue.reload
1206 1206 orig = Issue.find(1)
1207 1207 assert_equal orig.subject, issue.subject
1208 1208 assert_equal orig.tracker, issue.tracker
1209 1209 assert_equal "125", issue.custom_value_for(2).value
1210 1210 end
1211 1211
1212 1212 def test_copy_should_copy_status
1213 1213 orig = Issue.find(8)
1214 1214 assert orig.status != orig.default_status
1215 1215
1216 1216 issue = Issue.new.copy_from(orig)
1217 1217 assert issue.save
1218 1218 issue.reload
1219 1219 assert_equal orig.status, issue.status
1220 1220 end
1221 1221
1222 1222 def test_copy_should_add_relation_with_copied_issue
1223 1223 copied = Issue.find(1)
1224 1224 issue = Issue.new.copy_from(copied)
1225 1225 assert issue.save
1226 1226 issue.reload
1227 1227
1228 1228 assert_equal 1, issue.relations.size
1229 1229 relation = issue.relations.first
1230 1230 assert_equal 'copied_to', relation.relation_type
1231 1231 assert_equal copied, relation.issue_from
1232 1232 assert_equal issue, relation.issue_to
1233 1233 end
1234 1234
1235 1235 def test_copy_should_copy_subtasks
1236 1236 issue = Issue.generate_with_descendants!
1237 1237
1238 1238 copy = issue.reload.copy
1239 1239 copy.author = User.find(7)
1240 1240 assert_difference 'Issue.count', 1+issue.descendants.count do
1241 1241 assert copy.save
1242 1242 end
1243 1243 copy.reload
1244 1244 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
1245 1245 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1246 1246 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
1247 1247 assert_equal copy.author, child_copy.author
1248 1248 end
1249 1249
1250 1250 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
1251 1251 parent = Issue.generate!
1252 1252 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1253 1253 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1254 1254
1255 1255 copy = parent.reload.copy
1256 1256 copy.parent_issue_id = parent.id
1257 1257 copy.author = User.find(7)
1258 1258 assert_difference 'Issue.count', 3 do
1259 1259 assert copy.save
1260 1260 end
1261 1261 parent.reload
1262 1262 copy.reload
1263 1263 assert_equal parent, copy.parent
1264 1264 assert_equal 3, parent.children.count
1265 1265 assert_equal 5, parent.descendants.count
1266 1266 assert_equal 2, copy.children.count
1267 1267 assert_equal 2, copy.descendants.count
1268 1268 end
1269 1269
1270 1270 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
1271 1271 parent = Issue.generate!
1272 1272 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1273 1273 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1274 1274
1275 1275 copy = parent.reload.copy
1276 1276 copy.parent_issue_id = child1.id
1277 1277 copy.author = User.find(7)
1278 1278 assert_difference 'Issue.count', 3 do
1279 1279 assert copy.save
1280 1280 end
1281 1281 parent.reload
1282 1282 child1.reload
1283 1283 copy.reload
1284 1284 assert_equal child1, copy.parent
1285 1285 assert_equal 2, parent.children.count
1286 1286 assert_equal 5, parent.descendants.count
1287 1287 assert_equal 1, child1.children.count
1288 1288 assert_equal 3, child1.descendants.count
1289 1289 assert_equal 2, copy.children.count
1290 1290 assert_equal 2, copy.descendants.count
1291 1291 end
1292 1292
1293 1293 def test_copy_should_copy_subtasks_to_target_project
1294 1294 issue = Issue.generate_with_descendants!
1295 1295
1296 1296 copy = issue.copy(:project_id => 3)
1297 1297 assert_difference 'Issue.count', 1+issue.descendants.count do
1298 1298 assert copy.save
1299 1299 end
1300 1300 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
1301 1301 end
1302 1302
1303 1303 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
1304 1304 issue = Issue.generate_with_descendants!
1305 1305
1306 1306 copy = issue.reload.copy
1307 1307 assert_difference 'Issue.count', 1+issue.descendants.count do
1308 1308 assert copy.save
1309 1309 assert copy.save
1310 1310 end
1311 1311 end
1312 1312
1313 1313 def test_should_not_call_after_project_change_on_creation
1314 1314 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1315 1315 :subject => 'Test', :author_id => 1)
1316 1316 issue.expects(:after_project_change).never
1317 1317 issue.save!
1318 1318 end
1319 1319
1320 1320 def test_should_not_call_after_project_change_on_update
1321 1321 issue = Issue.find(1)
1322 1322 issue.project = Project.find(1)
1323 1323 issue.subject = 'No project change'
1324 1324 issue.expects(:after_project_change).never
1325 1325 issue.save!
1326 1326 end
1327 1327
1328 1328 def test_should_call_after_project_change_on_project_change
1329 1329 issue = Issue.find(1)
1330 1330 issue.project = Project.find(2)
1331 1331 issue.expects(:after_project_change).once
1332 1332 issue.save!
1333 1333 end
1334 1334
1335 1335 def test_adding_journal_should_update_timestamp
1336 1336 issue = Issue.find(1)
1337 1337 updated_on_was = issue.updated_on
1338 1338
1339 1339 issue.init_journal(User.first, "Adding notes")
1340 1340 assert_difference 'Journal.count' do
1341 1341 assert issue.save
1342 1342 end
1343 1343 issue.reload
1344 1344
1345 1345 assert_not_equal updated_on_was, issue.updated_on
1346 1346 end
1347 1347
1348 1348 def test_should_close_duplicates
1349 1349 # Create 3 issues
1350 1350 issue1 = Issue.generate!
1351 1351 issue2 = Issue.generate!
1352 1352 issue3 = Issue.generate!
1353 1353
1354 1354 # 2 is a dupe of 1
1355 1355 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1356 1356 :relation_type => IssueRelation::TYPE_DUPLICATES)
1357 1357 # And 3 is a dupe of 2
1358 1358 # IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1359 1359 # :relation_type => IssueRelation::TYPE_DUPLICATES)
1360 1360 # And 3 is a dupe of 1 (circular duplicates)
1361 1361 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1362 1362 :relation_type => IssueRelation::TYPE_DUPLICATES)
1363 1363
1364 1364 assert issue1.reload.duplicates.include?(issue2)
1365 1365
1366 1366 # Closing issue 1
1367 1367 issue1.init_journal(User.first, "Closing issue1")
1368 1368 issue1.status = IssueStatus.where(:is_closed => true).first
1369 1369 assert issue1.save
1370 1370 # 2 and 3 should be also closed
1371 1371 assert issue2.reload.closed?
1372 1372 assert issue3.reload.closed?
1373 1373 end
1374 1374
1375 1375 def test_should_close_duplicates_with_private_notes
1376 1376 issue = Issue.generate!
1377 1377 duplicate = Issue.generate!
1378 1378 IssueRelation.create!(:issue_from => duplicate, :issue_to => issue,
1379 1379 :relation_type => IssueRelation::TYPE_DUPLICATES)
1380 1380 assert issue.reload.duplicates.include?(duplicate)
1381 1381
1382 1382 # Closing issue with private notes
1383 1383 issue.init_journal(User.first, "Private notes")
1384 1384 issue.private_notes = true
1385 1385 issue.status = IssueStatus.where(:is_closed => true).first
1386 1386 assert_save issue
1387 1387
1388 1388 duplicate.reload
1389 1389 assert journal = duplicate.journals.detect {|journal| journal.notes == "Private notes"}
1390 1390 assert_equal true, journal.private_notes
1391 1391 end
1392 1392
1393 1393 def test_should_not_close_duplicated_issue
1394 1394 issue1 = Issue.generate!
1395 1395 issue2 = Issue.generate!
1396 1396
1397 1397 # 2 is a dupe of 1
1398 1398 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1399 1399 :relation_type => IssueRelation::TYPE_DUPLICATES)
1400 1400 # 2 is a dup of 1 but 1 is not a duplicate of 2
1401 1401 assert !issue2.reload.duplicates.include?(issue1)
1402 1402
1403 1403 # Closing issue 2
1404 1404 issue2.init_journal(User.first, "Closing issue2")
1405 1405 issue2.status = IssueStatus.where(:is_closed => true).first
1406 1406 assert issue2.save
1407 1407 # 1 should not be also closed
1408 1408 assert !issue1.reload.closed?
1409 1409 end
1410 1410
1411 1411 def test_assignable_versions
1412 1412 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1413 1413 :status_id => 1, :fixed_version_id => 1,
1414 1414 :subject => 'New issue')
1415 1415 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1416 1416 end
1417 1417
1418 1418 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1419 1419 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1420 1420 :status_id => 1, :fixed_version_id => 1,
1421 1421 :subject => 'New issue')
1422 1422 assert !issue.save
1423 1423 assert_not_equal [], issue.errors[:fixed_version_id]
1424 1424 end
1425 1425
1426 1426 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1427 1427 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1428 1428 :status_id => 1, :fixed_version_id => 2,
1429 1429 :subject => 'New issue')
1430 1430 assert !issue.save
1431 1431 assert_not_equal [], issue.errors[:fixed_version_id]
1432 1432 end
1433 1433
1434 1434 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1435 1435 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1436 1436 :status_id => 1, :fixed_version_id => 3,
1437 1437 :subject => 'New issue')
1438 1438 assert issue.save
1439 1439 end
1440 1440
1441 1441 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1442 1442 issue = Issue.find(11)
1443 1443 assert_equal 'closed', issue.fixed_version.status
1444 1444 issue.subject = 'Subject changed'
1445 1445 assert issue.save
1446 1446 end
1447 1447
1448 1448 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1449 1449 issue = Issue.find(11)
1450 1450 issue.status_id = 1
1451 1451 assert !issue.save
1452 1452 assert_not_equal [], issue.errors[:base]
1453 1453 end
1454 1454
1455 1455 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1456 1456 issue = Issue.find(11)
1457 1457 issue.status_id = 1
1458 1458 issue.fixed_version_id = 3
1459 1459 assert issue.save
1460 1460 end
1461 1461
1462 1462 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1463 1463 issue = Issue.find(12)
1464 1464 assert_equal 'locked', issue.fixed_version.status
1465 1465 issue.status_id = 1
1466 1466 assert issue.save
1467 1467 end
1468 1468
1469 1469 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1470 1470 issue = Issue.find(2)
1471 1471 assert_equal 2, issue.fixed_version_id
1472 1472 issue.project_id = 3
1473 1473 assert_nil issue.fixed_version_id
1474 1474 issue.fixed_version_id = 2
1475 1475 assert !issue.save
1476 1476 assert_include 'Target version is not included in the list', issue.errors.full_messages
1477 1477 end
1478 1478
1479 1479 def test_should_keep_shared_version_when_changing_project
1480 1480 Version.find(2).update_attribute :sharing, 'tree'
1481 1481
1482 1482 issue = Issue.find(2)
1483 1483 assert_equal 2, issue.fixed_version_id
1484 1484 issue.project_id = 3
1485 1485 assert_equal 2, issue.fixed_version_id
1486 1486 assert issue.save
1487 1487 end
1488 1488
1489 1489 def test_allowed_target_projects_should_include_projects_with_issue_tracking_enabled
1490 1490 assert_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1491 1491 end
1492 1492
1493 1493 def test_allowed_target_projects_should_not_include_projects_with_issue_tracking_disabled
1494 1494 Project.find(2).disable_module! :issue_tracking
1495 1495 assert_not_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1496 1496 end
1497 1497
1498 1498 def test_allowed_target_projects_should_not_include_projects_without_trackers
1499 1499 project = Project.generate!(:tracker_ids => [])
1500 1500 assert project.trackers.empty?
1501 1501 assert_not_include project, Issue.allowed_target_projects(User.find(1))
1502 1502 end
1503 1503
1504 1504 def test_allowed_target_trackers_with_one_role_allowed_on_all_trackers
1505 1505 user = User.generate!
1506 1506 role = Role.generate!
1507 1507 role.add_permission! :add_issues
1508 1508 role.set_permission_trackers :add_issues, :all
1509 1509 role.save!
1510 1510 User.add_to_project(user, Project.find(1), role)
1511 1511
1512 1512 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1513 1513 end
1514 1514
1515 1515 def test_allowed_target_trackers_with_one_role_allowed_on_some_trackers
1516 1516 user = User.generate!
1517 1517 role = Role.generate!
1518 1518 role.add_permission! :add_issues
1519 1519 role.set_permission_trackers :add_issues, [1, 3]
1520 1520 role.save!
1521 1521 User.add_to_project(user, Project.find(1), role)
1522 1522
1523 1523 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1524 1524 end
1525 1525
1526 1526 def test_allowed_target_trackers_with_two_roles_allowed_on_some_trackers
1527 1527 user = User.generate!
1528 1528 role1 = Role.generate!
1529 1529 role1.add_permission! :add_issues
1530 1530 role1.set_permission_trackers :add_issues, [1]
1531 1531 role1.save!
1532 1532 role2 = Role.generate!
1533 1533 role2.add_permission! :add_issues
1534 1534 role2.set_permission_trackers :add_issues, [3]
1535 1535 role2.save!
1536 1536 User.add_to_project(user, Project.find(1), [role1, role2])
1537 1537
1538 1538 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1539 1539 end
1540 1540
1541 1541 def test_allowed_target_trackers_with_two_roles_allowed_on_all_trackers_and_some_trackers
1542 1542 user = User.generate!
1543 1543 role1 = Role.generate!
1544 1544 role1.add_permission! :add_issues
1545 1545 role1.set_permission_trackers :add_issues, :all
1546 1546 role1.save!
1547 1547 role2 = Role.generate!
1548 1548 role2.add_permission! :add_issues
1549 1549 role2.set_permission_trackers :add_issues, [1, 3]
1550 1550 role2.save!
1551 1551 User.add_to_project(user, Project.find(1), [role1, role2])
1552 1552
1553 1553 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1554 1554 end
1555 1555
1556 1556 def test_allowed_target_trackers_should_not_consider_roles_without_add_issues_permission
1557 1557 user = User.generate!
1558 1558 role1 = Role.generate!
1559 1559 role1.remove_permission! :add_issues
1560 1560 role1.set_permission_trackers :add_issues, :all
1561 1561 role1.save!
1562 1562 role2 = Role.generate!
1563 1563 role2.add_permission! :add_issues
1564 1564 role2.set_permission_trackers :add_issues, [1, 3]
1565 1565 role2.save!
1566 1566 User.add_to_project(user, Project.find(1), [role1, role2])
1567 1567
1568 1568 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1569 1569 end
1570 1570
1571 1571 def test_allowed_target_trackers_without_project_should_be_empty
1572 1572 issue = Issue.new
1573 1573 assert_nil issue.project
1574 1574 assert_equal [], issue.allowed_target_trackers(User.find(2)).ids
1575 1575 end
1576 1576
1577 1577 def test_allowed_target_trackers_should_include_current_tracker
1578 1578 user = User.generate!
1579 1579 role = Role.generate!
1580 1580 role.add_permission! :add_issues
1581 1581 role.set_permission_trackers :add_issues, [3]
1582 1582 role.save!
1583 1583 User.add_to_project(user, Project.find(1), role)
1584 1584
1585 1585 issue = Issue.generate!(:project => Project.find(1), :tracker => Tracker.find(1))
1586 1586 assert_equal [1, 3], issue.allowed_target_trackers(user).ids.sort
1587 1587 end
1588 1588
1589 1589 def test_move_to_another_project_with_same_category
1590 1590 issue = Issue.find(1)
1591 1591 issue.project = Project.find(2)
1592 1592 assert issue.save
1593 1593 issue.reload
1594 1594 assert_equal 2, issue.project_id
1595 1595 # Category changes
1596 1596 assert_equal 4, issue.category_id
1597 1597 # Make sure time entries were move to the target project
1598 1598 assert_equal 2, issue.time_entries.first.project_id
1599 1599 end
1600 1600
1601 1601 def test_move_to_another_project_without_same_category
1602 1602 issue = Issue.find(2)
1603 1603 issue.project = Project.find(2)
1604 1604 assert issue.save
1605 1605 issue.reload
1606 1606 assert_equal 2, issue.project_id
1607 1607 # Category cleared
1608 1608 assert_nil issue.category_id
1609 1609 end
1610 1610
1611 1611 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1612 1612 issue = Issue.find(1)
1613 1613 issue.update_attribute(:fixed_version_id, 1)
1614 1614 issue.project = Project.find(2)
1615 1615 assert issue.save
1616 1616 issue.reload
1617 1617 assert_equal 2, issue.project_id
1618 1618 # Cleared fixed_version
1619 1619 assert_equal nil, issue.fixed_version
1620 1620 end
1621 1621
1622 1622 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1623 1623 issue = Issue.find(1)
1624 1624 issue.update_attribute(:fixed_version_id, 4)
1625 1625 issue.project = Project.find(5)
1626 1626 assert issue.save
1627 1627 issue.reload
1628 1628 assert_equal 5, issue.project_id
1629 1629 # Keep fixed_version
1630 1630 assert_equal 4, issue.fixed_version_id
1631 1631 end
1632 1632
1633 1633 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1634 1634 issue = Issue.find(1)
1635 1635 issue.update_attribute(:fixed_version_id, 1)
1636 1636 issue.project = Project.find(5)
1637 1637 assert issue.save
1638 1638 issue.reload
1639 1639 assert_equal 5, issue.project_id
1640 1640 # Cleared fixed_version
1641 1641 assert_equal nil, issue.fixed_version
1642 1642 end
1643 1643
1644 1644 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1645 1645 issue = Issue.find(1)
1646 1646 issue.update_attribute(:fixed_version_id, 7)
1647 1647 issue.project = Project.find(2)
1648 1648 assert issue.save
1649 1649 issue.reload
1650 1650 assert_equal 2, issue.project_id
1651 1651 # Keep fixed_version
1652 1652 assert_equal 7, issue.fixed_version_id
1653 1653 end
1654 1654
1655 1655 def test_move_to_another_project_should_keep_parent_if_valid
1656 1656 issue = Issue.find(1)
1657 1657 issue.update_attribute(:parent_issue_id, 2)
1658 1658 issue.project = Project.find(3)
1659 1659 assert issue.save
1660 1660 issue.reload
1661 1661 assert_equal 2, issue.parent_id
1662 1662 end
1663 1663
1664 1664 def test_move_to_another_project_should_clear_parent_if_not_valid
1665 1665 issue = Issue.find(1)
1666 1666 issue.update_attribute(:parent_issue_id, 2)
1667 1667 issue.project = Project.find(2)
1668 1668 assert issue.save
1669 1669 issue.reload
1670 1670 assert_nil issue.parent_id
1671 1671 end
1672 1672
1673 1673 def test_move_to_another_project_with_disabled_tracker
1674 1674 issue = Issue.find(1)
1675 1675 target = Project.find(2)
1676 1676 target.tracker_ids = [3]
1677 1677 target.save
1678 1678 issue.project = target
1679 1679 assert issue.save
1680 1680 issue.reload
1681 1681 assert_equal 2, issue.project_id
1682 1682 assert_equal 3, issue.tracker_id
1683 1683 end
1684 1684
1685 1685 def test_copy_to_the_same_project
1686 1686 issue = Issue.find(1)
1687 1687 copy = issue.copy
1688 1688 assert_difference 'Issue.count' do
1689 1689 copy.save!
1690 1690 end
1691 1691 assert_kind_of Issue, copy
1692 1692 assert_equal issue.project, copy.project
1693 1693 assert_equal "125", copy.custom_value_for(2).value
1694 1694 end
1695 1695
1696 1696 def test_copy_to_another_project_and_tracker
1697 1697 issue = Issue.find(1)
1698 1698 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1699 1699 assert_difference 'Issue.count' do
1700 1700 copy.save!
1701 1701 end
1702 1702 copy.reload
1703 1703 assert_kind_of Issue, copy
1704 1704 assert_equal Project.find(3), copy.project
1705 1705 assert_equal Tracker.find(2), copy.tracker
1706 1706 # Custom field #2 is not associated with target tracker
1707 1707 assert_nil copy.custom_value_for(2)
1708 1708 end
1709 1709
1710 1710 test "#copy should not create a journal" do
1711 1711 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :assigned_to_id => 3}, :link => false)
1712 1712 copy.save!
1713 1713 assert_equal 0, copy.reload.journals.size
1714 1714 end
1715 1715
1716 1716 test "#copy should allow assigned_to changes" do
1717 1717 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1718 1718 assert_equal 3, copy.assigned_to_id
1719 1719 end
1720 1720
1721 1721 test "#copy should allow status changes" do
1722 1722 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1723 1723 assert_equal 2, copy.status_id
1724 1724 end
1725 1725
1726 1726 test "#copy should allow start date changes" do
1727 1727 date = Date.today
1728 1728 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1729 1729 assert_equal date, copy.start_date
1730 1730 end
1731 1731
1732 1732 test "#copy should allow due date changes" do
1733 1733 date = Date.today
1734 1734 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1735 1735 assert_equal date, copy.due_date
1736 1736 end
1737 1737
1738 1738 test "#copy should set current user as author" do
1739 1739 User.current = User.find(9)
1740 1740 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1741 1741 assert_equal User.current, copy.author
1742 1742 end
1743 1743
1744 1744 test "#copy should create a journal with notes" do
1745 1745 date = Date.today
1746 1746 notes = "Notes added when copying"
1747 1747 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :start_date => date}, :link => false)
1748 1748 copy.init_journal(User.current, notes)
1749 1749 copy.save!
1750 1750
1751 1751 assert_equal 1, copy.journals.size
1752 1752 journal = copy.journals.first
1753 1753 assert_equal 0, journal.details.size
1754 1754 assert_equal notes, journal.notes
1755 1755 end
1756 1756
1757 1757 def test_valid_parent_project
1758 1758 issue = Issue.find(1)
1759 1759 issue_in_same_project = Issue.find(2)
1760 1760 issue_in_child_project = Issue.find(5)
1761 1761 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1762 1762 issue_in_other_child_project = Issue.find(6)
1763 1763 issue_in_different_tree = Issue.find(4)
1764 1764
1765 1765 with_settings :cross_project_subtasks => '' do
1766 1766 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1767 1767 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1768 1768 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1769 1769 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1770 1770 end
1771 1771
1772 1772 with_settings :cross_project_subtasks => 'system' do
1773 1773 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1774 1774 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1775 1775 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1776 1776 end
1777 1777
1778 1778 with_settings :cross_project_subtasks => 'tree' do
1779 1779 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1780 1780 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1781 1781 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1782 1782 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1783 1783
1784 1784 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1785 1785 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1786 1786 end
1787 1787
1788 1788 with_settings :cross_project_subtasks => 'descendants' do
1789 1789 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1790 1790 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1791 1791 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1792 1792 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1793 1793
1794 1794 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1795 1795 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1796 1796 end
1797 1797 end
1798 1798
1799 1799 def test_recipients_should_include_previous_assignee
1800 1800 user = User.find(3)
1801 1801 user.members.update_all ["mail_notification = ?", false]
1802 1802 user.update_attribute :mail_notification, 'only_assigned'
1803 1803
1804 1804 issue = Issue.find(2)
1805 1805 issue.assigned_to = nil
1806 1806 assert_include user.mail, issue.recipients
1807 1807 issue.save!
1808 1808 assert !issue.recipients.include?(user.mail)
1809 1809 end
1810 1810
1811 1811 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1812 1812 issue = Issue.find(12)
1813 1813 assert issue.recipients.include?(issue.author.mail)
1814 1814 # copy the issue to a private project
1815 1815 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1816 1816 # author is not a member of project anymore
1817 1817 assert !copy.recipients.include?(copy.author.mail)
1818 1818 end
1819 1819
1820 1820 def test_recipients_should_include_the_assigned_group_members
1821 1821 group_member = User.generate!
1822 1822 group = Group.generate!
1823 1823 group.users << group_member
1824 1824
1825 1825 issue = Issue.find(12)
1826 1826 issue.assigned_to = group
1827 1827 assert issue.recipients.include?(group_member.mail)
1828 1828 end
1829 1829
1830 1830 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1831 1831 user = User.find(3)
1832 1832 issue = Issue.find(9)
1833 1833 Watcher.create!(:user => user, :watchable => issue)
1834 1834 assert issue.watched_by?(user)
1835 1835 assert !issue.watcher_recipients.include?(user.mail)
1836 1836 end
1837 1837
1838 1838 def test_issue_destroy
1839 1839 Issue.find(1).destroy
1840 1840 assert_nil Issue.find_by_id(1)
1841 1841 assert_nil TimeEntry.find_by_issue_id(1)
1842 1842 end
1843 1843
1844 1844 def test_destroy_should_delete_time_entries_custom_values
1845 1845 issue = Issue.generate!
1846 1846 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1847 1847
1848 1848 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1849 1849 assert issue.destroy
1850 1850 end
1851 1851 end
1852 1852
1853 1853 def test_destroying_a_deleted_issue_should_not_raise_an_error
1854 1854 issue = Issue.find(1)
1855 1855 Issue.find(1).destroy
1856 1856
1857 1857 assert_nothing_raised do
1858 1858 assert_no_difference 'Issue.count' do
1859 1859 issue.destroy
1860 1860 end
1861 1861 assert issue.destroyed?
1862 1862 end
1863 1863 end
1864 1864
1865 1865 def test_destroying_a_stale_issue_should_not_raise_an_error
1866 1866 issue = Issue.find(1)
1867 1867 Issue.find(1).update_attribute :subject, "Updated"
1868 1868
1869 1869 assert_nothing_raised do
1870 1870 assert_difference 'Issue.count', -1 do
1871 1871 issue.destroy
1872 1872 end
1873 1873 assert issue.destroyed?
1874 1874 end
1875 1875 end
1876 1876
1877 1877 def test_blocked
1878 1878 blocked_issue = Issue.find(9)
1879 1879 blocking_issue = Issue.find(10)
1880 1880
1881 1881 assert blocked_issue.blocked?
1882 1882 assert !blocking_issue.blocked?
1883 1883 end
1884 1884
1885 1885 def test_blocked_issues_dont_allow_closed_statuses
1886 1886 blocked_issue = Issue.find(9)
1887 1887
1888 1888 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1889 1889 assert !allowed_statuses.empty?
1890 1890 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1891 1891 assert closed_statuses.empty?
1892 1892 end
1893 1893
1894 1894 def test_unblocked_issues_allow_closed_statuses
1895 1895 blocking_issue = Issue.find(10)
1896 1896
1897 1897 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1898 1898 assert !allowed_statuses.empty?
1899 1899 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1900 1900 assert !closed_statuses.empty?
1901 1901 end
1902 1902
1903 1903 def test_reschedule_an_issue_without_dates
1904 1904 with_settings :non_working_week_days => [] do
1905 1905 issue = Issue.new(:start_date => nil, :due_date => nil)
1906 1906 issue.reschedule_on '2012-10-09'.to_date
1907 1907 assert_equal '2012-10-09'.to_date, issue.start_date
1908 1908 assert_equal '2012-10-09'.to_date, issue.due_date
1909 1909 end
1910 1910
1911 1911 with_settings :non_working_week_days => %w(6 7) do
1912 1912 issue = Issue.new(:start_date => nil, :due_date => nil)
1913 1913 issue.reschedule_on '2012-10-09'.to_date
1914 1914 assert_equal '2012-10-09'.to_date, issue.start_date
1915 1915 assert_equal '2012-10-09'.to_date, issue.due_date
1916 1916
1917 1917 issue = Issue.new(:start_date => nil, :due_date => nil)
1918 1918 issue.reschedule_on '2012-10-13'.to_date
1919 1919 assert_equal '2012-10-15'.to_date, issue.start_date
1920 1920 assert_equal '2012-10-15'.to_date, issue.due_date
1921 1921 end
1922 1922 end
1923 1923
1924 1924 def test_reschedule_an_issue_with_start_date
1925 1925 with_settings :non_working_week_days => [] do
1926 1926 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1927 1927 issue.reschedule_on '2012-10-13'.to_date
1928 1928 assert_equal '2012-10-13'.to_date, issue.start_date
1929 1929 assert_equal '2012-10-13'.to_date, issue.due_date
1930 1930 end
1931 1931
1932 1932 with_settings :non_working_week_days => %w(6 7) do
1933 1933 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1934 1934 issue.reschedule_on '2012-10-11'.to_date
1935 1935 assert_equal '2012-10-11'.to_date, issue.start_date
1936 1936 assert_equal '2012-10-11'.to_date, issue.due_date
1937 1937
1938 1938 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1939 1939 issue.reschedule_on '2012-10-13'.to_date
1940 1940 assert_equal '2012-10-15'.to_date, issue.start_date
1941 1941 assert_equal '2012-10-15'.to_date, issue.due_date
1942 1942 end
1943 1943 end
1944 1944
1945 1945 def test_reschedule_an_issue_with_start_and_due_dates
1946 1946 with_settings :non_working_week_days => [] do
1947 1947 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1948 1948 issue.reschedule_on '2012-10-13'.to_date
1949 1949 assert_equal '2012-10-13'.to_date, issue.start_date
1950 1950 assert_equal '2012-10-19'.to_date, issue.due_date
1951 1951 end
1952 1952
1953 1953 with_settings :non_working_week_days => %w(6 7) do
1954 1954 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1955 1955 issue.reschedule_on '2012-10-11'.to_date
1956 1956 assert_equal '2012-10-11'.to_date, issue.start_date
1957 1957 assert_equal '2012-10-23'.to_date, issue.due_date
1958 1958
1959 1959 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1960 1960 issue.reschedule_on '2012-10-13'.to_date
1961 1961 assert_equal '2012-10-15'.to_date, issue.start_date
1962 1962 assert_equal '2012-10-25'.to_date, issue.due_date
1963 1963 end
1964 1964 end
1965 1965
1966 1966 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1967 1967 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1968 1968 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1969 1969 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1970 1970 :relation_type => IssueRelation::TYPE_PRECEDES)
1971 1971 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1972 1972
1973 1973 issue1.reload
1974 1974 issue1.due_date = '2012-10-23'
1975 1975 issue1.save!
1976 1976 issue2.reload
1977 1977 assert_equal Date.parse('2012-10-24'), issue2.start_date
1978 1978 assert_equal Date.parse('2012-10-26'), issue2.due_date
1979 1979 end
1980 1980
1981 1981 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1982 1982 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1983 1983 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1984 1984 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1985 1985 :relation_type => IssueRelation::TYPE_PRECEDES)
1986 1986 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1987 1987
1988 1988 issue1.reload
1989 1989 issue1.start_date = '2012-09-17'
1990 1990 issue1.due_date = '2012-09-18'
1991 1991 issue1.save!
1992 1992 issue2.reload
1993 1993 assert_equal Date.parse('2012-09-19'), issue2.start_date
1994 1994 assert_equal Date.parse('2012-09-21'), issue2.due_date
1995 1995 end
1996 1996
1997 1997 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1998 1998 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1999 1999 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2000 2000 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
2001 2001 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2002 2002 :relation_type => IssueRelation::TYPE_PRECEDES)
2003 2003 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
2004 2004 :relation_type => IssueRelation::TYPE_PRECEDES)
2005 2005 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
2006 2006
2007 2007 issue1.reload
2008 2008 issue1.start_date = '2012-09-17'
2009 2009 issue1.due_date = '2012-09-18'
2010 2010 issue1.save!
2011 2011 issue2.reload
2012 2012 # Issue 2 must start after Issue 3
2013 2013 assert_equal Date.parse('2012-10-03'), issue2.start_date
2014 2014 assert_equal Date.parse('2012-10-05'), issue2.due_date
2015 2015 end
2016 2016
2017 2017 def test_rescheduling_a_stale_issue_should_not_raise_an_error
2018 2018 with_settings :non_working_week_days => [] do
2019 2019 stale = Issue.find(1)
2020 2020 issue = Issue.find(1)
2021 2021 issue.subject = "Updated"
2022 2022 issue.save!
2023 2023 date = 10.days.from_now.to_date
2024 2024 assert_nothing_raised do
2025 2025 stale.reschedule_on!(date)
2026 2026 end
2027 2027 assert_equal date, stale.reload.start_date
2028 2028 end
2029 2029 end
2030 2030
2031 2031 def test_child_issue_should_consider_parent_soonest_start_on_create
2032 2032 set_language_if_valid 'en'
2033 2033 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2034 2034 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
2035 2035 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2036 2036 :relation_type => IssueRelation::TYPE_PRECEDES)
2037 2037 issue1.reload
2038 2038 issue2.reload
2039 2039 assert_equal Date.parse('2012-10-18'), issue2.start_date
2040 2040
2041 2041 with_settings :date_format => '%m/%d/%Y' do
2042 2042 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
2043 2043 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
2044 2044 assert !child.valid?
2045 2045 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
2046 2046 assert_equal Date.parse('2012-10-18'), child.soonest_start
2047 2047 child.start_date = '2012-10-18'
2048 2048 assert child.save
2049 2049 end
2050 2050 end
2051 2051
2052 2052 def test_setting_parent_to_a_an_issue_that_precedes_should_not_validate
2053 2053 # tests that 3 cannot have 1 as parent:
2054 2054 #
2055 2055 # 1 -> 2 -> 3
2056 2056 #
2057 2057 set_language_if_valid 'en'
2058 2058 issue1 = Issue.generate!
2059 2059 issue2 = Issue.generate!
2060 2060 issue3 = Issue.generate!
2061 2061 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2062 2062 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2063 2063 issue3.reload
2064 2064 issue3.parent_issue_id = issue1.id
2065 2065 assert !issue3.valid?
2066 2066 assert_include 'Parent task is invalid', issue3.errors.full_messages
2067 2067 end
2068 2068
2069 2069 def test_setting_parent_to_a_an_issue_that_follows_should_not_validate
2070 2070 # tests that 1 cannot have 3 as parent:
2071 2071 #
2072 2072 # 1 -> 2 -> 3
2073 2073 #
2074 2074 set_language_if_valid 'en'
2075 2075 issue1 = Issue.generate!
2076 2076 issue2 = Issue.generate!
2077 2077 issue3 = Issue.generate!
2078 2078 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2079 2079 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2080 2080 issue1.reload
2081 2081 issue1.parent_issue_id = issue3.id
2082 2082 assert !issue1.valid?
2083 2083 assert_include 'Parent task is invalid', issue1.errors.full_messages
2084 2084 end
2085 2085
2086 2086 def test_setting_parent_to_a_an_issue_that_precedes_through_hierarchy_should_not_validate
2087 2087 # tests that 4 cannot have 1 as parent:
2088 2088 # changing the due date of 4 would update the end date of 1 which would reschedule 2
2089 2089 # which would change the end date of 3 which would reschedule 4 and so on...
2090 2090 #
2091 2091 # 3 -> 4
2092 2092 # ^
2093 2093 # 1 -> 2
2094 2094 #
2095 2095 set_language_if_valid 'en'
2096 2096 issue1 = Issue.generate!
2097 2097 issue2 = Issue.generate!
2098 2098 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2099 2099 issue3 = Issue.generate!
2100 2100 issue2.reload
2101 2101 issue2.parent_issue_id = issue3.id
2102 2102 issue2.save!
2103 2103 issue4 = Issue.generate!
2104 2104 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
2105 2105 issue4.reload
2106 2106 issue4.parent_issue_id = issue1.id
2107 2107 assert !issue4.valid?
2108 2108 assert_include 'Parent task is invalid', issue4.errors.full_messages
2109 2109 end
2110 2110
2111 2111 def test_issue_and_following_issue_should_be_able_to_be_moved_to_the_same_parent
2112 2112 set_language_if_valid 'en'
2113 2113 issue1 = Issue.generate!
2114 2114 issue2 = Issue.generate!
2115 2115 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_FOLLOWS)
2116 2116 parent = Issue.generate!
2117 2117 issue1.reload.parent_issue_id = parent.id
2118 2118 assert_save issue1
2119 2119 parent.reload
2120 2120 issue2.reload.parent_issue_id = parent.id
2121 2121 assert_save issue2
2122 2122 assert IssueRelation.exists?(relation.id)
2123 2123 end
2124 2124
2125 2125 def test_issue_and_preceding_issue_should_be_able_to_be_moved_to_the_same_parent
2126 2126 set_language_if_valid 'en'
2127 2127 issue1 = Issue.generate!
2128 2128 issue2 = Issue.generate!
2129 2129 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
2130 2130 parent = Issue.generate!
2131 2131 issue1.reload.parent_issue_id = parent.id
2132 2132 assert_save issue1
2133 2133 parent.reload
2134 2134 issue2.reload.parent_issue_id = parent.id
2135 2135 assert_save issue2
2136 2136 assert IssueRelation.exists?(relation.id)
2137 2137 end
2138 2138
2139 2139 def test_issue_and_blocked_issue_should_be_able_to_be_moved_to_the_same_parent
2140 2140 set_language_if_valid 'en'
2141 2141 issue1 = Issue.generate!
2142 2142 issue2 = Issue.generate!
2143 2143 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKED)
2144 2144 parent = Issue.generate!
2145 2145 issue1.reload.parent_issue_id = parent.id
2146 2146 assert_save issue1
2147 2147 parent.reload
2148 2148 issue2.reload.parent_issue_id = parent.id
2149 2149 assert_save issue2
2150 2150 assert IssueRelation.exists?(relation.id)
2151 2151 end
2152 2152
2153 2153 def test_issue_and_blocking_issue_should_be_able_to_be_moved_to_the_same_parent
2154 2154 set_language_if_valid 'en'
2155 2155 issue1 = Issue.generate!
2156 2156 issue2 = Issue.generate!
2157 2157 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKS)
2158 2158 parent = Issue.generate!
2159 2159 issue1.reload.parent_issue_id = parent.id
2160 2160 assert_save issue1
2161 2161 parent.reload
2162 2162 issue2.reload.parent_issue_id = parent.id
2163 2163 assert_save issue2
2164 2164 assert IssueRelation.exists?(relation.id)
2165 2165 end
2166 2166
2167 2167 def test_issue_copy_should_be_able_to_be_moved_to_the_same_parent_as_copied_issue
2168 2168 issue = Issue.generate!
2169 2169 parent = Issue.generate!
2170 2170 issue.parent_issue_id = parent.id
2171 2171 issue.save!
2172 2172 issue.reload
2173 2173
2174 2174 copy = Issue.new.copy_from(issue, :link => true)
2175 2175 relation = new_record(IssueRelation) do
2176 2176 copy.save!
2177 2177 end
2178 2178
2179 2179 copy.parent_issue_id = parent.id
2180 2180 assert_save copy
2181 2181 assert IssueRelation.exists?(relation.id)
2182 2182 end
2183 2183
2184 2184 def test_overdue
2185 2185 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
2186 2186 assert !Issue.new(:due_date => Date.today).overdue?
2187 2187 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
2188 2188 assert !Issue.new(:due_date => nil).overdue?
2189 2189 assert !Issue.new(:due_date => 1.day.ago.to_date,
2190 2190 :status => IssueStatus.where(:is_closed => true).first
2191 2191 ).overdue?
2192 2192 end
2193 2193
2194 2194 test "#behind_schedule? should be false if the issue has no start_date" do
2195 2195 assert !Issue.new(:start_date => nil,
2196 2196 :due_date => 1.day.from_now.to_date,
2197 2197 :done_ratio => 0).behind_schedule?
2198 2198 end
2199 2199
2200 2200 test "#behind_schedule? should be false if the issue has no end_date" do
2201 2201 assert !Issue.new(:start_date => 1.day.from_now.to_date,
2202 2202 :due_date => nil,
2203 2203 :done_ratio => 0).behind_schedule?
2204 2204 end
2205 2205
2206 2206 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
2207 2207 assert !Issue.new(:start_date => 50.days.ago.to_date,
2208 2208 :due_date => 50.days.from_now.to_date,
2209 2209 :done_ratio => 90).behind_schedule?
2210 2210 end
2211 2211
2212 2212 test "#behind_schedule? should be true if the issue hasn't been started at all" do
2213 2213 assert Issue.new(:start_date => 1.day.ago.to_date,
2214 2214 :due_date => 1.day.from_now.to_date,
2215 2215 :done_ratio => 0).behind_schedule?
2216 2216 end
2217 2217
2218 2218 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
2219 2219 assert Issue.new(:start_date => 100.days.ago.to_date,
2220 2220 :due_date => Date.today,
2221 2221 :done_ratio => 90).behind_schedule?
2222 2222 end
2223 2223
2224 2224 test "#assignable_users should be Users" do
2225 2225 assert_kind_of User, Issue.find(1).assignable_users.first
2226 2226 end
2227 2227
2228 2228 test "#assignable_users should include the issue author" do
2229 2229 non_project_member = User.generate!
2230 2230 issue = Issue.generate!(:author => non_project_member)
2231 2231
2232 2232 assert issue.assignable_users.include?(non_project_member)
2233 2233 end
2234 2234
2235 2235 def test_assignable_users_should_not_include_anonymous_user
2236 2236 issue = Issue.generate!(:author => User.anonymous)
2237 2237
2238 2238 assert !issue.assignable_users.include?(User.anonymous)
2239 2239 end
2240 2240
2241 2241 def test_assignable_users_should_not_include_locked_user
2242 2242 user = User.generate!
2243 2243 issue = Issue.generate!(:author => user)
2244 2244 user.lock!
2245 2245
2246 2246 assert !issue.assignable_users.include?(user)
2247 2247 end
2248 2248
2249 2249 test "#assignable_users should include the current assignee" do
2250 2250 user = User.generate!
2251 2251 issue = Issue.generate!(:assigned_to => user)
2252 2252 user.lock!
2253 2253
2254 2254 assert Issue.find(issue.id).assignable_users.include?(user)
2255 2255 end
2256 2256
2257 2257 test "#assignable_users should not show the issue author twice" do
2258 2258 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
2259 2259 assert_equal 2, assignable_user_ids.length
2260 2260
2261 2261 assignable_user_ids.each do |user_id|
2262 2262 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
2263 2263 "User #{user_id} appears more or less than once"
2264 2264 end
2265 2265 end
2266 2266
2267 2267 test "#assignable_users with issue_group_assignment should include groups" do
2268 2268 issue = Issue.new(:project => Project.find(2))
2269 2269
2270 2270 with_settings :issue_group_assignment => '1' do
2271 2271 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2272 2272 assert issue.assignable_users.include?(Group.find(11))
2273 2273 end
2274 2274 end
2275 2275
2276 2276 test "#assignable_users without issue_group_assignment should not include groups" do
2277 2277 issue = Issue.new(:project => Project.find(2))
2278 2278
2279 2279 with_settings :issue_group_assignment => '0' do
2280 2280 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2281 2281 assert !issue.assignable_users.include?(Group.find(11))
2282 2282 end
2283 2283 end
2284 2284
2285 2285 def test_assignable_users_should_not_include_builtin_groups
2286 2286 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [1])
2287 2287 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [1])
2288 2288 issue = Issue.new(:project => Project.find(1))
2289 2289
2290 2290 with_settings :issue_group_assignment => '1' do
2291 2291 assert_nil issue.assignable_users.detect {|u| u.is_a?(GroupBuiltin)}
2292 2292 end
2293 2293 end
2294 2294
2295 def test_assignable_users_should_not_include_users_that_cannot_view_the_tracker
2296 user = User.find(3)
2297 role = Role.find(2)
2298 role.set_permission_trackers :view_issues, [1, 3]
2299 role.save!
2300
2301 issue1 = Issue.new(:project_id => 1, :tracker_id => 1)
2302 issue2 = Issue.new(:project_id => 1, :tracker_id => 2)
2303
2304 assert_include user, issue1.assignable_users
2305 assert_not_include user, issue2.assignable_users
2306 end
2307
2295 2308 def test_create_should_send_email_notification
2296 2309 ActionMailer::Base.deliveries.clear
2297 2310 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2298 2311 :author_id => 3, :status_id => 1,
2299 2312 :priority => IssuePriority.all.first,
2300 2313 :subject => 'test_create', :estimated_hours => '1:30')
2301 2314 with_settings :notified_events => %w(issue_added) do
2302 2315 assert issue.save
2303 2316 assert_equal 1, ActionMailer::Base.deliveries.size
2304 2317 end
2305 2318 end
2306 2319
2307 2320 def test_create_should_send_one_email_notification_with_both_settings
2308 2321 ActionMailer::Base.deliveries.clear
2309 2322 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2310 2323 :author_id => 3, :status_id => 1,
2311 2324 :priority => IssuePriority.all.first,
2312 2325 :subject => 'test_create', :estimated_hours => '1:30')
2313 2326 with_settings :notified_events => %w(issue_added issue_updated) do
2314 2327 assert issue.save
2315 2328 assert_equal 1, ActionMailer::Base.deliveries.size
2316 2329 end
2317 2330 end
2318 2331
2319 2332 def test_create_should_not_send_email_notification_with_no_setting
2320 2333 ActionMailer::Base.deliveries.clear
2321 2334 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2322 2335 :author_id => 3, :status_id => 1,
2323 2336 :priority => IssuePriority.all.first,
2324 2337 :subject => 'test_create', :estimated_hours => '1:30')
2325 2338 with_settings :notified_events => [] do
2326 2339 assert issue.save
2327 2340 assert_equal 0, ActionMailer::Base.deliveries.size
2328 2341 end
2329 2342 end
2330 2343
2331 2344 def test_update_should_notify_previous_assignee
2332 2345 ActionMailer::Base.deliveries.clear
2333 2346 user = User.find(3)
2334 2347 user.members.update_all ["mail_notification = ?", false]
2335 2348 user.update_attribute :mail_notification, 'only_assigned'
2336 2349
2337 2350 with_settings :notified_events => %w(issue_updated) do
2338 2351 issue = Issue.find(2)
2339 2352 issue.init_journal User.find(1)
2340 2353 issue.assigned_to = nil
2341 2354 issue.save!
2342 2355 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
2343 2356 end
2344 2357 end
2345 2358
2346 2359 def test_stale_issue_should_not_send_email_notification
2347 2360 ActionMailer::Base.deliveries.clear
2348 2361 issue = Issue.find(1)
2349 2362 stale = Issue.find(1)
2350 2363
2351 2364 issue.init_journal(User.find(1))
2352 2365 issue.subject = 'Subjet update'
2353 2366 with_settings :notified_events => %w(issue_updated) do
2354 2367 assert issue.save
2355 2368 assert_equal 1, ActionMailer::Base.deliveries.size
2356 2369 ActionMailer::Base.deliveries.clear
2357 2370
2358 2371 stale.init_journal(User.find(1))
2359 2372 stale.subject = 'Another subjet update'
2360 2373 assert_raise ActiveRecord::StaleObjectError do
2361 2374 stale.save
2362 2375 end
2363 2376 assert ActionMailer::Base.deliveries.empty?
2364 2377 end
2365 2378 end
2366 2379
2367 2380 def test_journalized_description
2368 2381 IssueCustomField.delete_all
2369 2382
2370 2383 i = Issue.first
2371 2384 old_description = i.description
2372 2385 new_description = "This is the new description"
2373 2386
2374 2387 i.init_journal(User.find(2))
2375 2388 i.description = new_description
2376 2389 assert_difference 'Journal.count', 1 do
2377 2390 assert_difference 'JournalDetail.count', 1 do
2378 2391 i.save!
2379 2392 end
2380 2393 end
2381 2394
2382 2395 detail = JournalDetail.order('id DESC').first
2383 2396 assert_equal i, detail.journal.journalized
2384 2397 assert_equal 'attr', detail.property
2385 2398 assert_equal 'description', detail.prop_key
2386 2399 assert_equal old_description, detail.old_value
2387 2400 assert_equal new_description, detail.value
2388 2401 end
2389 2402
2390 2403 def test_blank_descriptions_should_not_be_journalized
2391 2404 IssueCustomField.delete_all
2392 2405 Issue.where(:id => 1).update_all("description = NULL")
2393 2406
2394 2407 i = Issue.find(1)
2395 2408 i.init_journal(User.find(2))
2396 2409 i.subject = "blank description"
2397 2410 i.description = "\r\n"
2398 2411
2399 2412 assert_difference 'Journal.count', 1 do
2400 2413 assert_difference 'JournalDetail.count', 1 do
2401 2414 i.save!
2402 2415 end
2403 2416 end
2404 2417 end
2405 2418
2406 2419 def test_journalized_multi_custom_field
2407 2420 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
2408 2421 :is_filter => true, :is_for_all => true,
2409 2422 :tracker_ids => [1],
2410 2423 :possible_values => ['value1', 'value2', 'value3'],
2411 2424 :multiple => true)
2412 2425
2413 2426 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
2414 2427 :subject => 'Test', :author_id => 1)
2415 2428
2416 2429 assert_difference 'Journal.count' do
2417 2430 assert_difference 'JournalDetail.count' do
2418 2431 issue.init_journal(User.first)
2419 2432 issue.custom_field_values = {field.id => ['value1']}
2420 2433 issue.save!
2421 2434 end
2422 2435 assert_difference 'JournalDetail.count' do
2423 2436 issue.init_journal(User.first)
2424 2437 issue.custom_field_values = {field.id => ['value1', 'value2']}
2425 2438 issue.save!
2426 2439 end
2427 2440 assert_difference 'JournalDetail.count', 2 do
2428 2441 issue.init_journal(User.first)
2429 2442 issue.custom_field_values = {field.id => ['value3', 'value2']}
2430 2443 issue.save!
2431 2444 end
2432 2445 assert_difference 'JournalDetail.count', 2 do
2433 2446 issue.init_journal(User.first)
2434 2447 issue.custom_field_values = {field.id => nil}
2435 2448 issue.save!
2436 2449 end
2437 2450 end
2438 2451 end
2439 2452
2440 2453 def test_description_eol_should_be_normalized
2441 2454 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
2442 2455 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
2443 2456 end
2444 2457
2445 2458 def test_saving_twice_should_not_duplicate_journal_details
2446 2459 i = Issue.first
2447 2460 i.init_journal(User.find(2), 'Some notes')
2448 2461 # initial changes
2449 2462 i.subject = 'New subject'
2450 2463 i.done_ratio = i.done_ratio + 10
2451 2464 assert_difference 'Journal.count' do
2452 2465 assert i.save
2453 2466 end
2454 2467 # 1 more change
2455 2468 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
2456 2469 assert_no_difference 'Journal.count' do
2457 2470 assert_difference 'JournalDetail.count', 1 do
2458 2471 i.save
2459 2472 end
2460 2473 end
2461 2474 # no more change
2462 2475 assert_no_difference 'Journal.count' do
2463 2476 assert_no_difference 'JournalDetail.count' do
2464 2477 i.save
2465 2478 end
2466 2479 end
2467 2480 end
2468 2481
2469 2482 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2470 2483 @issue = Issue.find(1)
2471 2484 @issue_status = IssueStatus.find(1)
2472 2485 @issue_status.update_attribute(:default_done_ratio, 50)
2473 2486 @issue2 = Issue.find(2)
2474 2487 @issue_status2 = IssueStatus.find(2)
2475 2488 @issue_status2.update_attribute(:default_done_ratio, 0)
2476 2489
2477 2490 with_settings :issue_done_ratio => 'issue_field' do
2478 2491 assert_equal 0, @issue.done_ratio
2479 2492 assert_equal 30, @issue2.done_ratio
2480 2493 end
2481 2494
2482 2495 with_settings :issue_done_ratio => 'issue_status' do
2483 2496 assert_equal 50, @issue.done_ratio
2484 2497 assert_equal 0, @issue2.done_ratio
2485 2498 end
2486 2499 end
2487 2500
2488 2501 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2489 2502 @issue = Issue.find(1)
2490 2503 @issue_status = IssueStatus.find(1)
2491 2504 @issue_status.update_attribute(:default_done_ratio, 50)
2492 2505 @issue2 = Issue.find(2)
2493 2506 @issue_status2 = IssueStatus.find(2)
2494 2507 @issue_status2.update_attribute(:default_done_ratio, 0)
2495 2508
2496 2509 with_settings :issue_done_ratio => 'issue_field' do
2497 2510 @issue.update_done_ratio_from_issue_status
2498 2511 @issue2.update_done_ratio_from_issue_status
2499 2512
2500 2513 assert_equal 0, @issue.read_attribute(:done_ratio)
2501 2514 assert_equal 30, @issue2.read_attribute(:done_ratio)
2502 2515 end
2503 2516
2504 2517 with_settings :issue_done_ratio => 'issue_status' do
2505 2518 @issue.update_done_ratio_from_issue_status
2506 2519 @issue2.update_done_ratio_from_issue_status
2507 2520
2508 2521 assert_equal 50, @issue.read_attribute(:done_ratio)
2509 2522 assert_equal 0, @issue2.read_attribute(:done_ratio)
2510 2523 end
2511 2524 end
2512 2525
2513 2526 test "#by_tracker" do
2514 2527 User.current = User.anonymous
2515 2528 groups = Issue.by_tracker(Project.find(1))
2516 2529 assert_equal 3, groups.count
2517 2530 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2518 2531 end
2519 2532
2520 2533 test "#by_version" do
2521 2534 User.current = User.anonymous
2522 2535 groups = Issue.by_version(Project.find(1))
2523 2536 assert_equal 3, groups.count
2524 2537 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2525 2538 end
2526 2539
2527 2540 test "#by_priority" do
2528 2541 User.current = User.anonymous
2529 2542 groups = Issue.by_priority(Project.find(1))
2530 2543 assert_equal 4, groups.count
2531 2544 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2532 2545 end
2533 2546
2534 2547 test "#by_category" do
2535 2548 User.current = User.anonymous
2536 2549 groups = Issue.by_category(Project.find(1))
2537 2550 assert_equal 2, groups.count
2538 2551 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2539 2552 end
2540 2553
2541 2554 test "#by_assigned_to" do
2542 2555 User.current = User.anonymous
2543 2556 groups = Issue.by_assigned_to(Project.find(1))
2544 2557 assert_equal 2, groups.count
2545 2558 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2546 2559 end
2547 2560
2548 2561 test "#by_author" do
2549 2562 User.current = User.anonymous
2550 2563 groups = Issue.by_author(Project.find(1))
2551 2564 assert_equal 4, groups.count
2552 2565 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2553 2566 end
2554 2567
2555 2568 test "#by_subproject" do
2556 2569 User.current = User.anonymous
2557 2570 groups = Issue.by_subproject(Project.find(1))
2558 2571 # Private descendant not visible
2559 2572 assert_equal 1, groups.count
2560 2573 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2561 2574 end
2562 2575
2563 2576 def test_recently_updated_scope
2564 2577 #should return the last updated issue
2565 2578 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2566 2579 end
2567 2580
2568 2581 def test_on_active_projects_scope
2569 2582 assert Project.find(2).archive
2570 2583
2571 2584 before = Issue.on_active_project.length
2572 2585 # test inclusion to results
2573 2586 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2574 2587 assert_equal before + 1, Issue.on_active_project.length
2575 2588
2576 2589 # Move to an archived project
2577 2590 issue.project = Project.find(2)
2578 2591 assert issue.save
2579 2592 assert_equal before, Issue.on_active_project.length
2580 2593 end
2581 2594
2582 2595 test "Issue#recipients should include project recipients" do
2583 2596 issue = Issue.generate!
2584 2597 assert issue.project.recipients.present?
2585 2598 issue.project.recipients.each do |project_recipient|
2586 2599 assert issue.recipients.include?(project_recipient)
2587 2600 end
2588 2601 end
2589 2602
2590 2603 test "Issue#recipients should include the author if the author is active" do
2591 2604 issue = Issue.generate!(:author => User.generate!)
2592 2605 assert issue.author, "No author set for Issue"
2593 2606 assert issue.recipients.include?(issue.author.mail)
2594 2607 end
2595 2608
2596 2609 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2597 2610 issue = Issue.generate!(:assigned_to => User.generate!)
2598 2611 assert issue.assigned_to, "No assigned_to set for Issue"
2599 2612 assert issue.recipients.include?(issue.assigned_to.mail)
2600 2613 end
2601 2614
2602 2615 test "Issue#recipients should not include users who opt out of all email" do
2603 2616 issue = Issue.generate!(:author => User.generate!)
2604 2617 issue.author.update_attribute(:mail_notification, :none)
2605 2618 assert !issue.recipients.include?(issue.author.mail)
2606 2619 end
2607 2620
2608 2621 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2609 2622 issue = Issue.generate!(:author => User.generate!)
2610 2623 issue.author.update_attribute(:mail_notification, :only_assigned)
2611 2624 assert !issue.recipients.include?(issue.author.mail)
2612 2625 end
2613 2626
2614 2627 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2615 2628 issue = Issue.generate!(:assigned_to => User.generate!)
2616 2629 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2617 2630 assert !issue.recipients.include?(issue.assigned_to.mail)
2618 2631 end
2619 2632
2620 2633 def test_last_journal_id_with_journals_should_return_the_journal_id
2621 2634 assert_equal 2, Issue.find(1).last_journal_id
2622 2635 end
2623 2636
2624 2637 def test_last_journal_id_without_journals_should_return_nil
2625 2638 assert_nil Issue.find(3).last_journal_id
2626 2639 end
2627 2640
2628 2641 def test_journals_after_should_return_journals_with_greater_id
2629 2642 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2630 2643 assert_equal [], Issue.find(1).journals_after('2')
2631 2644 end
2632 2645
2633 2646 def test_journals_after_with_blank_arg_should_return_all_journals
2634 2647 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2635 2648 end
2636 2649
2637 2650 def test_css_classes_should_include_tracker
2638 2651 issue = Issue.new(:tracker => Tracker.find(2))
2639 2652 classes = issue.css_classes.split(' ')
2640 2653 assert_include 'tracker-2', classes
2641 2654 end
2642 2655
2643 2656 def test_css_classes_should_include_priority
2644 2657 issue = Issue.new(:priority => IssuePriority.find(8))
2645 2658 classes = issue.css_classes.split(' ')
2646 2659 assert_include 'priority-8', classes
2647 2660 assert_include 'priority-highest', classes
2648 2661 end
2649 2662
2650 2663 def test_css_classes_should_include_user_and_group_assignment
2651 2664 project = Project.first
2652 2665 user = User.generate!
2653 2666 group = Group.generate!
2654 2667 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2655 2668 group.users << user
2656 2669 assert user.member_of?(project)
2657 2670 issue1 = Issue.generate(:assigned_to_id => group.id)
2658 2671 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2659 2672 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2660 2673 issue2 = Issue.generate(:assigned_to_id => user.id)
2661 2674 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2662 2675 assert_include 'assigned-to-me', issue2.css_classes(user)
2663 2676 end
2664 2677
2665 2678 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2666 2679 set_tmp_attachments_directory
2667 2680 issue = Issue.generate!
2668 2681 issue.save_attachments({
2669 2682 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2670 2683 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2671 2684 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2672 2685 })
2673 2686 issue.attach_saved_attachments
2674 2687
2675 2688 assert_equal 3, issue.reload.attachments.count
2676 2689 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2677 2690 end
2678 2691
2679 2692 def test_save_attachments_with_array_should_warn_about_missing_tokens
2680 2693 set_tmp_attachments_directory
2681 2694 issue = Issue.generate!
2682 2695 issue.save_attachments([
2683 2696 {'token' => 'missing'}
2684 2697 ])
2685 2698 assert !issue.save
2686 2699 assert issue.errors[:base].present?
2687 2700 assert_equal 0, issue.reload.attachments.count
2688 2701 end
2689 2702
2690 2703 def test_closed_on_should_be_nil_when_creating_an_open_issue
2691 2704 issue = Issue.generate!(:status_id => 1).reload
2692 2705 assert !issue.closed?
2693 2706 assert_nil issue.closed_on
2694 2707 end
2695 2708
2696 2709 def test_closed_on_should_be_set_when_creating_a_closed_issue
2697 2710 issue = Issue.generate!(:status_id => 5).reload
2698 2711 assert issue.closed?
2699 2712 assert_not_nil issue.closed_on
2700 2713 assert_equal issue.updated_on, issue.closed_on
2701 2714 assert_equal issue.created_on, issue.closed_on
2702 2715 end
2703 2716
2704 2717 def test_closed_on_should_be_nil_when_updating_an_open_issue
2705 2718 issue = Issue.find(1)
2706 2719 issue.subject = 'Not closed yet'
2707 2720 issue.save!
2708 2721 issue.reload
2709 2722 assert_nil issue.closed_on
2710 2723 end
2711 2724
2712 2725 def test_closed_on_should_be_set_when_closing_an_open_issue
2713 2726 issue = Issue.find(1)
2714 2727 issue.subject = 'Now closed'
2715 2728 issue.status_id = 5
2716 2729 issue.save!
2717 2730 issue.reload
2718 2731 assert_not_nil issue.closed_on
2719 2732 assert_equal issue.updated_on, issue.closed_on
2720 2733 end
2721 2734
2722 2735 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2723 2736 issue = Issue.open(false).first
2724 2737 was_closed_on = issue.closed_on
2725 2738 assert_not_nil was_closed_on
2726 2739 issue.subject = 'Updating a closed issue'
2727 2740 issue.save!
2728 2741 issue.reload
2729 2742 assert_equal was_closed_on, issue.closed_on
2730 2743 end
2731 2744
2732 2745 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2733 2746 issue = Issue.open(false).first
2734 2747 was_closed_on = issue.closed_on
2735 2748 assert_not_nil was_closed_on
2736 2749 issue.subject = 'Reopening a closed issue'
2737 2750 issue.status_id = 1
2738 2751 issue.save!
2739 2752 issue.reload
2740 2753 assert !issue.closed?
2741 2754 assert_equal was_closed_on, issue.closed_on
2742 2755 end
2743 2756
2744 2757 def test_status_was_should_return_nil_for_new_issue
2745 2758 issue = Issue.new
2746 2759 assert_nil issue.status_was
2747 2760 end
2748 2761
2749 2762 def test_status_was_should_return_status_before_change
2750 2763 issue = Issue.find(1)
2751 2764 issue.status = IssueStatus.find(2)
2752 2765 assert_equal IssueStatus.find(1), issue.status_was
2753 2766 end
2754 2767
2755 2768 def test_status_was_should_return_status_before_change_with_status_id
2756 2769 issue = Issue.find(1)
2757 2770 assert_equal IssueStatus.find(1), issue.status
2758 2771 issue.status_id = 2
2759 2772 assert_equal IssueStatus.find(1), issue.status_was
2760 2773 end
2761 2774
2762 2775 def test_status_was_should_be_reset_on_save
2763 2776 issue = Issue.find(1)
2764 2777 issue.status = IssueStatus.find(2)
2765 2778 assert_equal IssueStatus.find(1), issue.status_was
2766 2779 assert issue.save!
2767 2780 assert_equal IssueStatus.find(2), issue.status_was
2768 2781 end
2769 2782
2770 2783 def test_closing_should_return_true_when_closing_an_issue
2771 2784 issue = Issue.find(1)
2772 2785 issue.status = IssueStatus.find(2)
2773 2786 assert_equal false, issue.closing?
2774 2787 issue.status = IssueStatus.find(5)
2775 2788 assert_equal true, issue.closing?
2776 2789 end
2777 2790
2778 2791 def test_closing_should_return_true_when_closing_an_issue_with_status_id
2779 2792 issue = Issue.find(1)
2780 2793 issue.status_id = 2
2781 2794 assert_equal false, issue.closing?
2782 2795 issue.status_id = 5
2783 2796 assert_equal true, issue.closing?
2784 2797 end
2785 2798
2786 2799 def test_closing_should_return_true_for_new_closed_issue
2787 2800 issue = Issue.new
2788 2801 assert_equal false, issue.closing?
2789 2802 issue.status = IssueStatus.find(5)
2790 2803 assert_equal true, issue.closing?
2791 2804 end
2792 2805
2793 2806 def test_closing_should_return_true_for_new_closed_issue_with_status_id
2794 2807 issue = Issue.new
2795 2808 assert_equal false, issue.closing?
2796 2809 issue.status_id = 5
2797 2810 assert_equal true, issue.closing?
2798 2811 end
2799 2812
2800 2813 def test_closing_should_be_reset_after_save
2801 2814 issue = Issue.find(1)
2802 2815 issue.status_id = 5
2803 2816 assert_equal true, issue.closing?
2804 2817 issue.save!
2805 2818 assert_equal false, issue.closing?
2806 2819 end
2807 2820
2808 2821 def test_reopening_should_return_true_when_reopening_an_issue
2809 2822 issue = Issue.find(8)
2810 2823 issue.status = IssueStatus.find(6)
2811 2824 assert_equal false, issue.reopening?
2812 2825 issue.status = IssueStatus.find(2)
2813 2826 assert_equal true, issue.reopening?
2814 2827 end
2815 2828
2816 2829 def test_reopening_should_return_true_when_reopening_an_issue_with_status_id
2817 2830 issue = Issue.find(8)
2818 2831 issue.status_id = 6
2819 2832 assert_equal false, issue.reopening?
2820 2833 issue.status_id = 2
2821 2834 assert_equal true, issue.reopening?
2822 2835 end
2823 2836
2824 2837 def test_reopening_should_return_false_for_new_open_issue
2825 2838 issue = Issue.new
2826 2839 issue.status = IssueStatus.find(1)
2827 2840 assert_equal false, issue.reopening?
2828 2841 end
2829 2842
2830 2843 def test_reopening_should_be_reset_after_save
2831 2844 issue = Issue.find(8)
2832 2845 issue.status_id = 2
2833 2846 assert_equal true, issue.reopening?
2834 2847 issue.save!
2835 2848 assert_equal false, issue.reopening?
2836 2849 end
2837 2850
2838 2851 def test_default_status_without_tracker_should_be_nil
2839 2852 issue = Issue.new
2840 2853 assert_nil issue.tracker
2841 2854 assert_nil issue.default_status
2842 2855 end
2843 2856
2844 2857 def test_default_status_should_be_tracker_default_status
2845 2858 issue = Issue.new(:tracker_id => 1)
2846 2859 assert_not_nil issue.status
2847 2860 assert_equal issue.tracker.default_status, issue.default_status
2848 2861 end
2849 2862
2850 2863 def test_initializing_with_tracker_should_set_default_status
2851 2864 issue = Issue.new(:tracker => Tracker.find(1))
2852 2865 assert_not_nil issue.status
2853 2866 assert_equal issue.default_status, issue.status
2854 2867 end
2855 2868
2856 2869 def test_initializing_with_tracker_id_should_set_default_status
2857 2870 issue = Issue.new(:tracker_id => 1)
2858 2871 assert_not_nil issue.status
2859 2872 assert_equal issue.default_status, issue.status
2860 2873 end
2861 2874
2862 2875 def test_setting_tracker_should_set_default_status
2863 2876 issue = Issue.new
2864 2877 issue.tracker = Tracker.find(1)
2865 2878 assert_not_nil issue.status
2866 2879 assert_equal issue.default_status, issue.status
2867 2880 end
2868 2881
2869 2882 def test_changing_tracker_should_set_default_status_if_status_was_default
2870 2883 WorkflowTransition.delete_all
2871 2884 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1
2872 2885 Tracker.find(2).update! :default_status_id => 2
2873 2886
2874 2887 issue = Issue.new(:tracker_id => 1, :status_id => 1)
2875 2888 assert_equal IssueStatus.find(1), issue.status
2876 2889 issue.tracker = Tracker.find(2)
2877 2890 assert_equal IssueStatus.find(2), issue.status
2878 2891 end
2879 2892
2880 2893 def test_changing_tracker_should_set_default_status_if_status_is_not_used_by_tracker
2881 2894 WorkflowTransition.delete_all
2882 2895 Tracker.find(2).update! :default_status_id => 2
2883 2896
2884 2897 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2885 2898 assert_equal IssueStatus.find(3), issue.status
2886 2899 issue.tracker = Tracker.find(2)
2887 2900 assert_equal IssueStatus.find(2), issue.status
2888 2901 end
2889 2902
2890 2903 def test_changing_tracker_should_keep_status_if_status_was_not_default_and_is_used_by_tracker
2891 2904 WorkflowTransition.delete_all
2892 2905 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3
2893 2906 Tracker.find(2).update! :default_status_id => 2
2894 2907
2895 2908 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2896 2909 assert_equal IssueStatus.find(3), issue.status
2897 2910 issue.tracker = Tracker.find(2)
2898 2911 assert_equal IssueStatus.find(3), issue.status
2899 2912 end
2900 2913
2901 2914 def test_assigned_to_was_with_a_group
2902 2915 group = Group.find(10)
2903 2916
2904 2917 issue = Issue.generate!(:assigned_to => group)
2905 2918 issue.reload.assigned_to = nil
2906 2919 assert_equal group, issue.assigned_to_was
2907 2920 end
2908 2921
2909 2922 def test_issue_overdue_should_respect_user_timezone
2910 2923 user_in_europe = users(:users_001)
2911 2924 user_in_europe.pref.update_attribute :time_zone, 'UTC'
2912 2925
2913 2926 user_in_asia = users(:users_002)
2914 2927 user_in_asia.pref.update_attribute :time_zone, 'Hongkong'
2915 2928
2916 2929 issue = Issue.generate! :due_date => Date.parse('2016-03-20')
2917 2930
2918 2931 # server time is UTC
2919 2932 time = Time.parse '2016-03-20 20:00 UTC'
2920 2933 Time.stubs(:now).returns(time)
2921 2934 Date.stubs(:today).returns(time.to_date)
2922 2935
2923 2936 # for a user in the same time zone as the server the issue is not overdue
2924 2937 # yet
2925 2938 User.current = user_in_europe
2926 2939 assert !issue.overdue?
2927 2940
2928 2941 # at the same time, a user in East Asia looks at the issue - it's already
2929 2942 # March 21st and the issue should be marked overdue
2930 2943 User.current = user_in_asia
2931 2944 assert issue.overdue?
2932 2945
2933 2946 end
2934 2947 end
General Comments 0
You need to be logged in to leave comments. Login now