##// END OF EJS Templates
Always authorize admin users....
Jean-Philippe Lang -
r15093:c87783d50d55
parent child
Show More
@@ -1,1699 +1,1703
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),
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 # Returns true if user or current user is allowed to add notes to the issue
186 186 def notes_addable?(user=User.current)
187 187 user_tracker_permission?(user, :add_issue_notes)
188 188 end
189 189
190 190 # Returns true if user or current user is allowed to delete the issue
191 191 def deletable?(user=User.current)
192 192 user_tracker_permission?(user, :delete_issues)
193 193 end
194 194
195 195 def initialize(attributes=nil, *args)
196 196 super
197 197 if new_record?
198 198 # set default values for new records only
199 199 self.priority ||= IssuePriority.default
200 200 self.watcher_user_ids = []
201 201 end
202 202 end
203 203
204 204 def create_or_update
205 205 super
206 206 ensure
207 207 @status_was = nil
208 208 end
209 209 private :create_or_update
210 210
211 211 # AR#Persistence#destroy would raise and RecordNotFound exception
212 212 # if the issue was already deleted or updated (non matching lock_version).
213 213 # This is a problem when bulk deleting issues or deleting a project
214 214 # (because an issue may already be deleted if its parent was deleted
215 215 # first).
216 216 # The issue is reloaded by the nested_set before being deleted so
217 217 # the lock_version condition should not be an issue but we handle it.
218 218 def destroy
219 219 super
220 220 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
221 221 # Stale or already deleted
222 222 begin
223 223 reload
224 224 rescue ActiveRecord::RecordNotFound
225 225 # The issue was actually already deleted
226 226 @destroyed = true
227 227 return freeze
228 228 end
229 229 # The issue was stale, retry to destroy
230 230 super
231 231 end
232 232
233 233 alias :base_reload :reload
234 234 def reload(*args)
235 235 @workflow_rule_by_attribute = nil
236 236 @assignable_versions = nil
237 237 @relations = nil
238 238 @spent_hours = nil
239 239 @total_spent_hours = nil
240 240 @total_estimated_hours = nil
241 241 base_reload(*args)
242 242 end
243 243
244 244 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
245 245 def available_custom_fields
246 246 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
247 247 end
248 248
249 249 def visible_custom_field_values(user=nil)
250 250 user_real = user || User.current
251 251 custom_field_values.select do |value|
252 252 value.custom_field.visible_by?(project, user_real)
253 253 end
254 254 end
255 255
256 256 # Copies attributes from another issue, arg can be an id or an Issue
257 257 def copy_from(arg, options={})
258 258 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
259 259 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
260 260 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
261 261 self.status = issue.status
262 262 self.author = User.current
263 263 unless options[:attachments] == false
264 264 self.attachments = issue.attachments.map do |attachement|
265 265 attachement.copy(:container => self)
266 266 end
267 267 end
268 268 @copied_from = issue
269 269 @copy_options = options
270 270 self
271 271 end
272 272
273 273 # Returns an unsaved copy of the issue
274 274 def copy(attributes=nil, copy_options={})
275 275 copy = self.class.new.copy_from(self, copy_options)
276 276 copy.attributes = attributes if attributes
277 277 copy
278 278 end
279 279
280 280 # Returns true if the issue is a copy
281 281 def copy?
282 282 @copied_from.present?
283 283 end
284 284
285 285 def status_id=(status_id)
286 286 if status_id.to_s != self.status_id.to_s
287 287 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
288 288 end
289 289 self.status_id
290 290 end
291 291
292 292 # Sets the status.
293 293 def status=(status)
294 294 if status != self.status
295 295 @workflow_rule_by_attribute = nil
296 296 end
297 297 association(:status).writer(status)
298 298 end
299 299
300 300 def priority_id=(pid)
301 301 self.priority = nil
302 302 write_attribute(:priority_id, pid)
303 303 end
304 304
305 305 def category_id=(cid)
306 306 self.category = nil
307 307 write_attribute(:category_id, cid)
308 308 end
309 309
310 310 def fixed_version_id=(vid)
311 311 self.fixed_version = nil
312 312 write_attribute(:fixed_version_id, vid)
313 313 end
314 314
315 315 def tracker_id=(tracker_id)
316 316 if tracker_id.to_s != self.tracker_id.to_s
317 317 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
318 318 end
319 319 self.tracker_id
320 320 end
321 321
322 322 # Sets the tracker.
323 323 # This will set the status to the default status of the new tracker if:
324 324 # * the status was the default for the previous tracker
325 325 # * or if the status was not part of the new tracker statuses
326 326 # * or the status was nil
327 327 def tracker=(tracker)
328 328 tracker_was = self.tracker
329 329 association(:tracker).writer(tracker)
330 330 if tracker != tracker_was
331 331 if status == tracker_was.try(:default_status)
332 332 self.status = nil
333 333 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
334 334 self.status = nil
335 335 end
336 336 reassign_custom_field_values
337 337 @workflow_rule_by_attribute = nil
338 338 end
339 339 self.status ||= default_status
340 340 self.tracker
341 341 end
342 342
343 343 def project_id=(project_id)
344 344 if project_id.to_s != self.project_id.to_s
345 345 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
346 346 end
347 347 self.project_id
348 348 end
349 349
350 350 # Sets the project.
351 351 # Unless keep_tracker argument is set to true, this will change the tracker
352 352 # to the first tracker of the new project if the previous tracker is not part
353 353 # of the new project trackers.
354 354 # This will:
355 355 # * clear the fixed_version is it's no longer valid for the new project.
356 356 # * clear the parent issue if it's no longer valid for the new project.
357 357 # * set the category to the category with the same name in the new
358 358 # project if it exists, or clear it if it doesn't.
359 359 # * for new issue, set the fixed_version to the project default version
360 360 # if it's a valid fixed_version.
361 361 def project=(project, keep_tracker=false)
362 362 project_was = self.project
363 363 association(:project).writer(project)
364 364 if project_was && project && project_was != project
365 365 @assignable_versions = nil
366 366
367 367 unless keep_tracker || project.trackers.include?(tracker)
368 368 self.tracker = project.trackers.first
369 369 end
370 370 # Reassign to the category with same name if any
371 371 if category
372 372 self.category = project.issue_categories.find_by_name(category.name)
373 373 end
374 374 # Keep the fixed_version if it's still valid in the new_project
375 375 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
376 376 self.fixed_version = nil
377 377 end
378 378 # Clear the parent task if it's no longer valid
379 379 unless valid_parent_project?
380 380 self.parent_issue_id = nil
381 381 end
382 382 reassign_custom_field_values
383 383 @workflow_rule_by_attribute = nil
384 384 end
385 385 # Set fixed_version to the project default version if it's valid
386 386 if new_record? && fixed_version.nil? && project && project.default_version_id?
387 387 if project.shared_versions.open.exists?(project.default_version_id)
388 388 self.fixed_version_id = project.default_version_id
389 389 end
390 390 end
391 391 self.project
392 392 end
393 393
394 394 def description=(arg)
395 395 if arg.is_a?(String)
396 396 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
397 397 end
398 398 write_attribute(:description, arg)
399 399 end
400 400
401 401 # Overrides assign_attributes so that project and tracker get assigned first
402 402 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
403 403 return if new_attributes.nil?
404 404 attrs = new_attributes.dup
405 405 attrs.stringify_keys!
406 406
407 407 %w(project project_id tracker tracker_id).each do |attr|
408 408 if attrs.has_key?(attr)
409 409 send "#{attr}=", attrs.delete(attr)
410 410 end
411 411 end
412 412 send :assign_attributes_without_project_and_tracker_first, attrs, *args
413 413 end
414 414 # Do not redefine alias chain on reload (see #4838)
415 415 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
416 416
417 417 def attributes=(new_attributes)
418 418 assign_attributes new_attributes
419 419 end
420 420
421 421 def estimated_hours=(h)
422 422 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
423 423 end
424 424
425 425 safe_attributes 'project_id',
426 426 'tracker_id',
427 427 'status_id',
428 428 'category_id',
429 429 'assigned_to_id',
430 430 'priority_id',
431 431 'fixed_version_id',
432 432 'subject',
433 433 'description',
434 434 'start_date',
435 435 'due_date',
436 436 'done_ratio',
437 437 'estimated_hours',
438 438 'custom_field_values',
439 439 'custom_fields',
440 440 'lock_version',
441 441 'notes',
442 442 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
443 443
444 444 safe_attributes 'notes',
445 445 :if => lambda {|issue, user| issue.notes_addable?(user)}
446 446
447 447 safe_attributes 'private_notes',
448 448 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
449 449
450 450 safe_attributes 'watcher_user_ids',
451 451 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
452 452
453 453 safe_attributes 'is_private',
454 454 :if => lambda {|issue, user|
455 455 user.allowed_to?(:set_issues_private, issue.project) ||
456 456 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
457 457 }
458 458
459 459 safe_attributes 'parent_issue_id',
460 460 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
461 461 user.allowed_to?(:manage_subtasks, issue.project)}
462 462
463 463 def safe_attribute_names(user=nil)
464 464 names = super
465 465 names -= disabled_core_fields
466 466 names -= read_only_attribute_names(user)
467 467 if new_record?
468 468 # Make sure that project_id can always be set for new issues
469 469 names |= %w(project_id)
470 470 end
471 471 if dates_derived?
472 472 names -= %w(start_date due_date)
473 473 end
474 474 if priority_derived?
475 475 names -= %w(priority_id)
476 476 end
477 477 if done_ratio_derived?
478 478 names -= %w(done_ratio)
479 479 end
480 480 names
481 481 end
482 482
483 483 # Safely sets attributes
484 484 # Should be called from controllers instead of #attributes=
485 485 # attr_accessible is too rough because we still want things like
486 486 # Issue.new(:project => foo) to work
487 487 def safe_attributes=(attrs, user=User.current)
488 488 return unless attrs.is_a?(Hash)
489 489
490 490 attrs = attrs.deep_dup
491 491
492 492 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
493 493 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
494 494 if allowed_target_projects(user).where(:id => p.to_i).exists?
495 495 self.project_id = p
496 496 end
497 497
498 498 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
499 499 # Discard submitted category on previous project
500 500 attrs.delete('category_id')
501 501 end
502 502 end
503 503
504 504 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
505 505 if allowed_target_trackers(user).where(:id => t.to_i).exists?
506 506 self.tracker_id = t
507 507 end
508 508 end
509 509 if project
510 510 # Set a default tracker to accept custom field values
511 511 # even if tracker is not specified
512 512 self.tracker ||= allowed_target_trackers(user).first
513 513 end
514 514
515 515 statuses_allowed = new_statuses_allowed_to(user)
516 516 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
517 517 if statuses_allowed.collect(&:id).include?(s.to_i)
518 518 self.status_id = s
519 519 end
520 520 end
521 521 if new_record? && !statuses_allowed.include?(status)
522 522 self.status = statuses_allowed.first || default_status
523 523 end
524 524 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
525 525 if u.blank?
526 526 self.assigned_to_id = nil
527 527 else
528 528 u = u.to_i
529 529 if assignable_users.any?{|assignable_user| assignable_user.id == u}
530 530 self.assigned_to_id = u
531 531 end
532 532 end
533 533 end
534 534
535 535
536 536 attrs = delete_unsafe_attributes(attrs, user)
537 537 return if attrs.empty?
538 538
539 539 if attrs['parent_issue_id'].present?
540 540 s = attrs['parent_issue_id'].to_s
541 541 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
542 542 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
543 543 end
544 544 end
545 545
546 546 if attrs['custom_field_values'].present?
547 547 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
548 548 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
549 549 end
550 550
551 551 if attrs['custom_fields'].present?
552 552 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
553 553 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
554 554 end
555 555
556 556 # mass-assignment security bypass
557 557 assign_attributes attrs, :without_protection => true
558 558 end
559 559
560 560 def disabled_core_fields
561 561 tracker ? tracker.disabled_core_fields : []
562 562 end
563 563
564 564 # Returns the custom_field_values that can be edited by the given user
565 565 def editable_custom_field_values(user=nil)
566 566 visible_custom_field_values(user).reject do |value|
567 567 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
568 568 end
569 569 end
570 570
571 571 # Returns the custom fields that can be edited by the given user
572 572 def editable_custom_fields(user=nil)
573 573 editable_custom_field_values(user).map(&:custom_field).uniq
574 574 end
575 575
576 576 # Returns the names of attributes that are read-only for user or the current user
577 577 # For users with multiple roles, the read-only fields are the intersection of
578 578 # read-only fields of each role
579 579 # The result is an array of strings where sustom fields are represented with their ids
580 580 #
581 581 # Examples:
582 582 # issue.read_only_attribute_names # => ['due_date', '2']
583 583 # issue.read_only_attribute_names(user) # => []
584 584 def read_only_attribute_names(user=nil)
585 585 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
586 586 end
587 587
588 588 # Returns the names of required attributes for user or the current user
589 589 # For users with multiple roles, the required fields are the intersection of
590 590 # required fields of each role
591 591 # The result is an array of strings where sustom fields are represented with their ids
592 592 #
593 593 # Examples:
594 594 # issue.required_attribute_names # => ['due_date', '2']
595 595 # issue.required_attribute_names(user) # => []
596 596 def required_attribute_names(user=nil)
597 597 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
598 598 end
599 599
600 600 # Returns true if the attribute is required for user
601 601 def required_attribute?(name, user=nil)
602 602 required_attribute_names(user).include?(name.to_s)
603 603 end
604 604
605 605 # Returns a hash of the workflow rule by attribute for the given user
606 606 #
607 607 # Examples:
608 608 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
609 609 def workflow_rule_by_attribute(user=nil)
610 610 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
611 611
612 612 user_real = user || User.current
613 613 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
614 614 roles = roles.select(&:consider_workflow?)
615 615 return {} if roles.empty?
616 616
617 617 result = {}
618 618 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
619 619 if workflow_permissions.any?
620 620 workflow_rules = workflow_permissions.inject({}) do |h, wp|
621 621 h[wp.field_name] ||= {}
622 622 h[wp.field_name][wp.role_id] = wp.rule
623 623 h
624 624 end
625 625 fields_with_roles = {}
626 626 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
627 627 fields_with_roles[field_id] ||= []
628 628 fields_with_roles[field_id] << role_id
629 629 end
630 630 roles.each do |role|
631 631 fields_with_roles.each do |field_id, role_ids|
632 632 unless role_ids.include?(role.id)
633 633 field_name = field_id.to_s
634 634 workflow_rules[field_name] ||= {}
635 635 workflow_rules[field_name][role.id] = 'readonly'
636 636 end
637 637 end
638 638 end
639 639 workflow_rules.each do |attr, rules|
640 640 next if rules.size < roles.size
641 641 uniq_rules = rules.values.uniq
642 642 if uniq_rules.size == 1
643 643 result[attr] = uniq_rules.first
644 644 else
645 645 result[attr] = 'required'
646 646 end
647 647 end
648 648 end
649 649 @workflow_rule_by_attribute = result if user.nil?
650 650 result
651 651 end
652 652 private :workflow_rule_by_attribute
653 653
654 654 def done_ratio
655 655 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
656 656 status.default_done_ratio
657 657 else
658 658 read_attribute(:done_ratio)
659 659 end
660 660 end
661 661
662 662 def self.use_status_for_done_ratio?
663 663 Setting.issue_done_ratio == 'issue_status'
664 664 end
665 665
666 666 def self.use_field_for_done_ratio?
667 667 Setting.issue_done_ratio == 'issue_field'
668 668 end
669 669
670 670 def validate_issue
671 671 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
672 672 errors.add :due_date, :greater_than_start_date
673 673 end
674 674
675 675 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
676 676 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
677 677 end
678 678
679 679 if fixed_version
680 680 if !assignable_versions.include?(fixed_version)
681 681 errors.add :fixed_version_id, :inclusion
682 682 elsif reopening? && fixed_version.closed?
683 683 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
684 684 end
685 685 end
686 686
687 687 # Checks that the issue can not be added/moved to a disabled tracker
688 688 if project && (tracker_id_changed? || project_id_changed?)
689 689 unless project.trackers.include?(tracker)
690 690 errors.add :tracker_id, :inclusion
691 691 end
692 692 end
693 693
694 694 # Checks parent issue assignment
695 695 if @invalid_parent_issue_id.present?
696 696 errors.add :parent_issue_id, :invalid
697 697 elsif @parent_issue
698 698 if !valid_parent_project?(@parent_issue)
699 699 errors.add :parent_issue_id, :invalid
700 700 elsif (@parent_issue != parent) && (
701 701 self.would_reschedule?(@parent_issue) ||
702 702 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
703 703 )
704 704 errors.add :parent_issue_id, :invalid
705 705 elsif !new_record?
706 706 # moving an existing issue
707 707 if move_possible?(@parent_issue)
708 708 # move accepted
709 709 else
710 710 errors.add :parent_issue_id, :invalid
711 711 end
712 712 end
713 713 end
714 714 end
715 715
716 716 # Validates the issue against additional workflow requirements
717 717 def validate_required_fields
718 718 user = new_record? ? author : current_journal.try(:user)
719 719
720 720 required_attribute_names(user).each do |attribute|
721 721 if attribute =~ /^\d+$/
722 722 attribute = attribute.to_i
723 723 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
724 724 if v && Array(v.value).detect(&:present?).nil?
725 725 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
726 726 end
727 727 else
728 728 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
729 729 next if attribute == 'category_id' && project.try(:issue_categories).blank?
730 730 next if attribute == 'fixed_version_id' && assignable_versions.blank?
731 731 errors.add attribute, :blank
732 732 end
733 733 end
734 734 end
735 735 end
736 736
737 737 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
738 738 # so that custom values that are not editable are not validated (eg. a custom field that
739 739 # is marked as required should not trigger a validation error if the user is not allowed
740 740 # to edit this field).
741 741 def validate_custom_field_values
742 742 user = new_record? ? author : current_journal.try(:user)
743 743 if new_record? || custom_field_values_changed?
744 744 editable_custom_field_values(user).each(&:validate_value)
745 745 end
746 746 end
747 747
748 748 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
749 749 # even if the user turns off the setting later
750 750 def update_done_ratio_from_issue_status
751 751 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
752 752 self.done_ratio = status.default_done_ratio
753 753 end
754 754 end
755 755
756 756 def init_journal(user, notes = "")
757 757 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
758 758 end
759 759
760 760 # Returns the current journal or nil if it's not initialized
761 761 def current_journal
762 762 @current_journal
763 763 end
764 764
765 765 # Returns the names of attributes that are journalized when updating the issue
766 766 def journalized_attribute_names
767 767 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
768 768 if tracker
769 769 names -= tracker.disabled_core_fields
770 770 end
771 771 names
772 772 end
773 773
774 774 # Returns the id of the last journal or nil
775 775 def last_journal_id
776 776 if new_record?
777 777 nil
778 778 else
779 779 journals.maximum(:id)
780 780 end
781 781 end
782 782
783 783 # Returns a scope for journals that have an id greater than journal_id
784 784 def journals_after(journal_id)
785 785 scope = journals.reorder("#{Journal.table_name}.id ASC")
786 786 if journal_id.present?
787 787 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
788 788 end
789 789 scope
790 790 end
791 791
792 792 # Returns the initial status of the issue
793 793 # Returns nil for a new issue
794 794 def status_was
795 795 if status_id_changed?
796 796 if status_id_was.to_i > 0
797 797 @status_was ||= IssueStatus.find_by_id(status_id_was)
798 798 end
799 799 else
800 800 @status_was ||= status
801 801 end
802 802 end
803 803
804 804 # Return true if the issue is closed, otherwise false
805 805 def closed?
806 806 status.present? && status.is_closed?
807 807 end
808 808
809 809 # Returns true if the issue was closed when loaded
810 810 def was_closed?
811 811 status_was.present? && status_was.is_closed?
812 812 end
813 813
814 814 # Return true if the issue is being reopened
815 815 def reopening?
816 816 if new_record?
817 817 false
818 818 else
819 819 status_id_changed? && !closed? && was_closed?
820 820 end
821 821 end
822 822 alias :reopened? :reopening?
823 823
824 824 # Return true if the issue is being closed
825 825 def closing?
826 826 if new_record?
827 827 closed?
828 828 else
829 829 status_id_changed? && closed? && !was_closed?
830 830 end
831 831 end
832 832
833 833 # Returns true if the issue is overdue
834 834 def overdue?
835 835 due_date.present? && (due_date < User.current.today) && !closed?
836 836 end
837 837
838 838 # Is the amount of work done less than it should for the due date
839 839 def behind_schedule?
840 840 return false if start_date.nil? || due_date.nil?
841 841 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
842 842 return done_date <= User.current.today
843 843 end
844 844
845 845 # Does this issue have children?
846 846 def children?
847 847 !leaf?
848 848 end
849 849
850 850 # Users the issue can be assigned to
851 851 def assignable_users
852 852 users = project.assignable_users.to_a
853 853 users << author if author && author.active?
854 854 users << assigned_to if assigned_to
855 855 users.uniq.sort
856 856 end
857 857
858 858 # Versions that the issue can be assigned to
859 859 def assignable_versions
860 860 return @assignable_versions if @assignable_versions
861 861
862 862 versions = project.shared_versions.open.to_a
863 863 if fixed_version
864 864 if fixed_version_id_changed?
865 865 # nothing to do
866 866 elsif project_id_changed?
867 867 if project.shared_versions.include?(fixed_version)
868 868 versions << fixed_version
869 869 end
870 870 else
871 871 versions << fixed_version
872 872 end
873 873 end
874 874 @assignable_versions = versions.uniq.sort
875 875 end
876 876
877 877 # Returns true if this issue is blocked by another issue that is still open
878 878 def blocked?
879 879 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
880 880 end
881 881
882 882 # Returns the default status of the issue based on its tracker
883 883 # Returns nil if tracker is nil
884 884 def default_status
885 885 tracker.try(:default_status)
886 886 end
887 887
888 888 # Returns an array of statuses that user is able to apply
889 889 def new_statuses_allowed_to(user=User.current, include_default=false)
890 890 if new_record? && @copied_from
891 891 [default_status, @copied_from.status].compact.uniq.sort
892 892 else
893 893 initial_status = nil
894 894 if new_record?
895 895 # nop
896 896 elsif tracker_id_changed?
897 897 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
898 898 initial_status = default_status
899 899 elsif tracker.issue_status_ids.include?(status_id_was)
900 900 initial_status = IssueStatus.find_by_id(status_id_was)
901 901 else
902 902 initial_status = default_status
903 903 end
904 904 else
905 905 initial_status = status_was
906 906 end
907 907
908 908 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
909 909 assignee_transitions_allowed = initial_assigned_to_id.present? &&
910 910 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
911 911
912 912 statuses = []
913 913 statuses += IssueStatus.new_statuses_allowed(
914 914 initial_status,
915 915 user.admin ? Role.all.to_a : user.roles_for_project(project),
916 916 tracker,
917 917 author == user,
918 918 assignee_transitions_allowed
919 919 )
920 920 statuses << initial_status unless statuses.empty?
921 921 statuses << default_status if include_default || (new_record? && statuses.empty?)
922 922 statuses = statuses.compact.uniq.sort
923 923 if blocked?
924 924 statuses.reject!(&:is_closed?)
925 925 end
926 926 statuses
927 927 end
928 928 end
929 929
930 930 # Returns the previous assignee (user or group) if changed
931 931 def assigned_to_was
932 932 # assigned_to_id_was is reset before after_save callbacks
933 933 user_id = @previous_assigned_to_id || assigned_to_id_was
934 934 if user_id && user_id != assigned_to_id
935 935 @assigned_to_was ||= Principal.find_by_id(user_id)
936 936 end
937 937 end
938 938
939 939 # Returns the original tracker
940 940 def tracker_was
941 941 Tracker.find_by_id(tracker_id_was)
942 942 end
943 943
944 944 # Returns the users that should be notified
945 945 def notified_users
946 946 notified = []
947 947 # Author and assignee are always notified unless they have been
948 948 # locked or don't want to be notified
949 949 notified << author if author
950 950 if assigned_to
951 951 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
952 952 end
953 953 if assigned_to_was
954 954 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
955 955 end
956 956 notified = notified.select {|u| u.active? && u.notify_about?(self)}
957 957
958 958 notified += project.notified_users
959 959 notified.uniq!
960 960 # Remove users that can not view the issue
961 961 notified.reject! {|user| !visible?(user)}
962 962 notified
963 963 end
964 964
965 965 # Returns the email addresses that should be notified
966 966 def recipients
967 967 notified_users.collect(&:mail)
968 968 end
969 969
970 970 def each_notification(users, &block)
971 971 if users.any?
972 972 if custom_field_values.detect {|value| !value.custom_field.visible?}
973 973 users_by_custom_field_visibility = users.group_by do |user|
974 974 visible_custom_field_values(user).map(&:custom_field_id).sort
975 975 end
976 976 users_by_custom_field_visibility.values.each do |users|
977 977 yield(users)
978 978 end
979 979 else
980 980 yield(users)
981 981 end
982 982 end
983 983 end
984 984
985 985 def notify?
986 986 @notify != false
987 987 end
988 988
989 989 def notify=(arg)
990 990 @notify = arg
991 991 end
992 992
993 993 # Returns the number of hours spent on this issue
994 994 def spent_hours
995 995 @spent_hours ||= time_entries.sum(:hours) || 0
996 996 end
997 997
998 998 # Returns the total number of hours spent on this issue and its descendants
999 999 def total_spent_hours
1000 1000 @total_spent_hours ||= if leaf?
1001 1001 spent_hours
1002 1002 else
1003 1003 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1004 1004 end
1005 1005 end
1006 1006
1007 1007 def total_estimated_hours
1008 1008 if leaf?
1009 1009 estimated_hours
1010 1010 else
1011 1011 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1012 1012 end
1013 1013 end
1014 1014
1015 1015 def relations
1016 1016 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1017 1017 end
1018 1018
1019 1019 # Preloads relations for a collection of issues
1020 1020 def self.load_relations(issues)
1021 1021 if issues.any?
1022 1022 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1023 1023 issues.each do |issue|
1024 1024 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1025 1025 end
1026 1026 end
1027 1027 end
1028 1028
1029 1029 # Preloads visible spent time for a collection of issues
1030 1030 def self.load_visible_spent_hours(issues, user=User.current)
1031 1031 if issues.any?
1032 1032 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1033 1033 issues.each do |issue|
1034 1034 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1035 1035 end
1036 1036 end
1037 1037 end
1038 1038
1039 1039 # Preloads visible total spent time for a collection of issues
1040 1040 def self.load_visible_total_spent_hours(issues, user=User.current)
1041 1041 if issues.any?
1042 1042 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1043 1043 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1044 1044 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1045 1045 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1046 1046 issues.each do |issue|
1047 1047 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1048 1048 end
1049 1049 end
1050 1050 end
1051 1051
1052 1052 # Preloads visible relations for a collection of issues
1053 1053 def self.load_visible_relations(issues, user=User.current)
1054 1054 if issues.any?
1055 1055 issue_ids = issues.map(&:id)
1056 1056 # Relations with issue_from in given issues and visible issue_to
1057 1057 relations_from = IssueRelation.joins(:issue_to => :project).
1058 1058 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1059 1059 # Relations with issue_to in given issues and visible issue_from
1060 1060 relations_to = IssueRelation.joins(:issue_from => :project).
1061 1061 where(visible_condition(user)).
1062 1062 where(:issue_to_id => issue_ids).to_a
1063 1063 issues.each do |issue|
1064 1064 relations =
1065 1065 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1066 1066 relations_to.select {|relation| relation.issue_to_id == issue.id}
1067 1067
1068 1068 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1069 1069 end
1070 1070 end
1071 1071 end
1072 1072
1073 1073 # Finds an issue relation given its id.
1074 1074 def find_relation(relation_id)
1075 1075 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1076 1076 end
1077 1077
1078 1078 # Returns true if this issue blocks the other issue, otherwise returns false
1079 1079 def blocks?(other)
1080 1080 all = [self]
1081 1081 last = [self]
1082 1082 while last.any?
1083 1083 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1084 1084 current -= last
1085 1085 current -= all
1086 1086 return true if current.include?(other)
1087 1087 last = current
1088 1088 all += last
1089 1089 end
1090 1090 false
1091 1091 end
1092 1092
1093 1093 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1094 1094 def would_reschedule?(other)
1095 1095 all = [self]
1096 1096 last = [self]
1097 1097 while last.any?
1098 1098 current = last.map {|i|
1099 1099 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1100 1100 i.leaves.to_a +
1101 1101 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1102 1102 }.flatten.uniq
1103 1103 current -= last
1104 1104 current -= all
1105 1105 return true if current.include?(other)
1106 1106 last = current
1107 1107 all += last
1108 1108 end
1109 1109 false
1110 1110 end
1111 1111
1112 1112 # Returns an array of issues that duplicate this one
1113 1113 def duplicates
1114 1114 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1115 1115 end
1116 1116
1117 1117 # Returns the due date or the target due date if any
1118 1118 # Used on gantt chart
1119 1119 def due_before
1120 1120 due_date || (fixed_version ? fixed_version.effective_date : nil)
1121 1121 end
1122 1122
1123 1123 # Returns the time scheduled for this issue.
1124 1124 #
1125 1125 # Example:
1126 1126 # Start Date: 2/26/09, End Date: 3/04/09
1127 1127 # duration => 6
1128 1128 def duration
1129 1129 (start_date && due_date) ? due_date - start_date : 0
1130 1130 end
1131 1131
1132 1132 # Returns the duration in working days
1133 1133 def working_duration
1134 1134 (start_date && due_date) ? working_days(start_date, due_date) : 0
1135 1135 end
1136 1136
1137 1137 def soonest_start(reload=false)
1138 1138 if @soonest_start.nil? || reload
1139 1139 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1140 1140 p = @parent_issue || parent
1141 1141 if p && Setting.parent_issue_dates == 'derived'
1142 1142 dates << p.soonest_start
1143 1143 end
1144 1144 @soonest_start = dates.compact.max
1145 1145 end
1146 1146 @soonest_start
1147 1147 end
1148 1148
1149 1149 # Sets start_date on the given date or the next working day
1150 1150 # and changes due_date to keep the same working duration.
1151 1151 def reschedule_on(date)
1152 1152 wd = working_duration
1153 1153 date = next_working_date(date)
1154 1154 self.start_date = date
1155 1155 self.due_date = add_working_days(date, wd)
1156 1156 end
1157 1157
1158 1158 # Reschedules the issue on the given date or the next working day and saves the record.
1159 1159 # If the issue is a parent task, this is done by rescheduling its subtasks.
1160 1160 def reschedule_on!(date)
1161 1161 return if date.nil?
1162 1162 if leaf? || !dates_derived?
1163 1163 if start_date.nil? || start_date != date
1164 1164 if start_date && start_date > date
1165 1165 # Issue can not be moved earlier than its soonest start date
1166 1166 date = [soonest_start(true), date].compact.max
1167 1167 end
1168 1168 reschedule_on(date)
1169 1169 begin
1170 1170 save
1171 1171 rescue ActiveRecord::StaleObjectError
1172 1172 reload
1173 1173 reschedule_on(date)
1174 1174 save
1175 1175 end
1176 1176 end
1177 1177 else
1178 1178 leaves.each do |leaf|
1179 1179 if leaf.start_date
1180 1180 # Only move subtask if it starts at the same date as the parent
1181 1181 # or if it starts before the given date
1182 1182 if start_date == leaf.start_date || date > leaf.start_date
1183 1183 leaf.reschedule_on!(date)
1184 1184 end
1185 1185 else
1186 1186 leaf.reschedule_on!(date)
1187 1187 end
1188 1188 end
1189 1189 end
1190 1190 end
1191 1191
1192 1192 def dates_derived?
1193 1193 !leaf? && Setting.parent_issue_dates == 'derived'
1194 1194 end
1195 1195
1196 1196 def priority_derived?
1197 1197 !leaf? && Setting.parent_issue_priority == 'derived'
1198 1198 end
1199 1199
1200 1200 def done_ratio_derived?
1201 1201 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1202 1202 end
1203 1203
1204 1204 def <=>(issue)
1205 1205 if issue.nil?
1206 1206 -1
1207 1207 elsif root_id != issue.root_id
1208 1208 (root_id || 0) <=> (issue.root_id || 0)
1209 1209 else
1210 1210 (lft || 0) <=> (issue.lft || 0)
1211 1211 end
1212 1212 end
1213 1213
1214 1214 def to_s
1215 1215 "#{tracker} ##{id}: #{subject}"
1216 1216 end
1217 1217
1218 1218 # Returns a string of css classes that apply to the issue
1219 1219 def css_classes(user=User.current)
1220 1220 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1221 1221 s << ' closed' if closed?
1222 1222 s << ' overdue' if overdue?
1223 1223 s << ' child' if child?
1224 1224 s << ' parent' unless leaf?
1225 1225 s << ' private' if is_private?
1226 1226 if user.logged?
1227 1227 s << ' created-by-me' if author_id == user.id
1228 1228 s << ' assigned-to-me' if assigned_to_id == user.id
1229 1229 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1230 1230 end
1231 1231 s
1232 1232 end
1233 1233
1234 1234 # Unassigns issues from +version+ if it's no longer shared with issue's project
1235 1235 def self.update_versions_from_sharing_change(version)
1236 1236 # Update issues assigned to the version
1237 1237 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1238 1238 end
1239 1239
1240 1240 # Unassigns issues from versions that are no longer shared
1241 1241 # after +project+ was moved
1242 1242 def self.update_versions_from_hierarchy_change(project)
1243 1243 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1244 1244 # Update issues of the moved projects and issues assigned to a version of a moved project
1245 1245 Issue.update_versions(
1246 1246 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1247 1247 moved_project_ids, moved_project_ids]
1248 1248 )
1249 1249 end
1250 1250
1251 1251 def parent_issue_id=(arg)
1252 1252 s = arg.to_s.strip.presence
1253 1253 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1254 1254 @invalid_parent_issue_id = nil
1255 1255 elsif s.blank?
1256 1256 @parent_issue = nil
1257 1257 @invalid_parent_issue_id = nil
1258 1258 else
1259 1259 @parent_issue = nil
1260 1260 @invalid_parent_issue_id = arg
1261 1261 end
1262 1262 end
1263 1263
1264 1264 def parent_issue_id
1265 1265 if @invalid_parent_issue_id
1266 1266 @invalid_parent_issue_id
1267 1267 elsif instance_variable_defined? :@parent_issue
1268 1268 @parent_issue.nil? ? nil : @parent_issue.id
1269 1269 else
1270 1270 parent_id
1271 1271 end
1272 1272 end
1273 1273
1274 1274 def set_parent_id
1275 1275 self.parent_id = parent_issue_id
1276 1276 end
1277 1277
1278 1278 # Returns true if issue's project is a valid
1279 1279 # parent issue project
1280 1280 def valid_parent_project?(issue=parent)
1281 1281 return true if issue.nil? || issue.project_id == project_id
1282 1282
1283 1283 case Setting.cross_project_subtasks
1284 1284 when 'system'
1285 1285 true
1286 1286 when 'tree'
1287 1287 issue.project.root == project.root
1288 1288 when 'hierarchy'
1289 1289 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1290 1290 when 'descendants'
1291 1291 issue.project.is_or_is_ancestor_of?(project)
1292 1292 else
1293 1293 false
1294 1294 end
1295 1295 end
1296 1296
1297 1297 # Returns an issue scope based on project and scope
1298 1298 def self.cross_project_scope(project, scope=nil)
1299 1299 if project.nil?
1300 1300 return Issue
1301 1301 end
1302 1302 case scope
1303 1303 when 'all', 'system'
1304 1304 Issue
1305 1305 when 'tree'
1306 1306 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1307 1307 :lft => project.root.lft, :rgt => project.root.rgt)
1308 1308 when 'hierarchy'
1309 1309 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)",
1310 1310 :lft => project.lft, :rgt => project.rgt)
1311 1311 when 'descendants'
1312 1312 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1313 1313 :lft => project.lft, :rgt => project.rgt)
1314 1314 else
1315 1315 Issue.where(:project_id => project.id)
1316 1316 end
1317 1317 end
1318 1318
1319 1319 def self.by_tracker(project)
1320 1320 count_and_group_by(:project => project, :association => :tracker)
1321 1321 end
1322 1322
1323 1323 def self.by_version(project)
1324 1324 count_and_group_by(:project => project, :association => :fixed_version)
1325 1325 end
1326 1326
1327 1327 def self.by_priority(project)
1328 1328 count_and_group_by(:project => project, :association => :priority)
1329 1329 end
1330 1330
1331 1331 def self.by_category(project)
1332 1332 count_and_group_by(:project => project, :association => :category)
1333 1333 end
1334 1334
1335 1335 def self.by_assigned_to(project)
1336 1336 count_and_group_by(:project => project, :association => :assigned_to)
1337 1337 end
1338 1338
1339 1339 def self.by_author(project)
1340 1340 count_and_group_by(:project => project, :association => :author)
1341 1341 end
1342 1342
1343 1343 def self.by_subproject(project)
1344 1344 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1345 1345 r.reject {|r| r["project_id"] == project.id.to_s}
1346 1346 end
1347 1347
1348 1348 # Query generator for selecting groups of issue counts for a project
1349 1349 # based on specific criteria
1350 1350 #
1351 1351 # Options
1352 1352 # * project - Project to search in.
1353 1353 # * with_subprojects - Includes subprojects issues if set to true.
1354 1354 # * association - Symbol. Association for grouping.
1355 1355 def self.count_and_group_by(options)
1356 1356 assoc = reflect_on_association(options[:association])
1357 1357 select_field = assoc.foreign_key
1358 1358
1359 1359 Issue.
1360 1360 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1361 1361 joins(:status, assoc.name).
1362 1362 group(:status_id, :is_closed, select_field).
1363 1363 count.
1364 1364 map do |columns, total|
1365 1365 status_id, is_closed, field_value = columns
1366 1366 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1367 1367 {
1368 1368 "status_id" => status_id.to_s,
1369 1369 "closed" => is_closed,
1370 1370 select_field => field_value.to_s,
1371 1371 "total" => total.to_s
1372 1372 }
1373 1373 end
1374 1374 end
1375 1375
1376 1376 # Returns a scope of projects that user can assign the issue to
1377 1377 def allowed_target_projects(user=User.current)
1378 1378 current_project = new_record? ? nil : project
1379 1379 self.class.allowed_target_projects(user, current_project)
1380 1380 end
1381 1381
1382 1382 # Returns a scope of projects that user can assign issues to
1383 1383 # If current_project is given, it will be included in the scope
1384 1384 def self.allowed_target_projects(user=User.current, current_project=nil)
1385 1385 condition = Project.allowed_to_condition(user, :add_issues)
1386 1386 if current_project
1387 1387 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1388 1388 end
1389 1389 Project.where(condition).having_trackers
1390 1390 end
1391 1391
1392 1392 # Returns a scope of trackers that user can assign the issue to
1393 1393 def allowed_target_trackers(user=User.current)
1394 1394 self.class.allowed_target_trackers(project, user, tracker_id_was)
1395 1395 end
1396 1396
1397 1397 # Returns a scope of trackers that user can assign project issues to
1398 1398 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1399 1399 if project
1400 1400 scope = project.trackers.sorted
1401 1401 unless user.admin?
1402 1402 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1403 1403 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1404 1404 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1405 1405 if current_tracker
1406 1406 tracker_ids << current_tracker
1407 1407 end
1408 1408 scope = scope.where(:id => tracker_ids)
1409 1409 end
1410 1410 end
1411 1411 scope
1412 1412 else
1413 1413 Tracker.none
1414 1414 end
1415 1415 end
1416 1416
1417 1417 private
1418 1418
1419 1419 def user_tracker_permission?(user, permission)
1420 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1421 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1420 if user.admin?
1421 true
1422 else
1423 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1424 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1425 end
1422 1426 end
1423 1427
1424 1428 def after_project_change
1425 1429 # Update project_id on related time entries
1426 1430 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1427 1431
1428 1432 # Delete issue relations
1429 1433 unless Setting.cross_project_issue_relations?
1430 1434 relations_from.clear
1431 1435 relations_to.clear
1432 1436 end
1433 1437
1434 1438 # Move subtasks that were in the same project
1435 1439 children.each do |child|
1436 1440 next unless child.project_id == project_id_was
1437 1441 # Change project and keep project
1438 1442 child.send :project=, project, true
1439 1443 unless child.save
1440 1444 raise ActiveRecord::Rollback
1441 1445 end
1442 1446 end
1443 1447 end
1444 1448
1445 1449 # Callback for after the creation of an issue by copy
1446 1450 # * adds a "copied to" relation with the copied issue
1447 1451 # * copies subtasks from the copied issue
1448 1452 def after_create_from_copy
1449 1453 return unless copy? && !@after_create_from_copy_handled
1450 1454
1451 1455 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1452 1456 if @current_journal
1453 1457 @copied_from.init_journal(@current_journal.user)
1454 1458 end
1455 1459 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1456 1460 unless relation.save
1457 1461 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1458 1462 end
1459 1463 end
1460 1464
1461 1465 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1462 1466 copy_options = (@copy_options || {}).merge(:subtasks => false)
1463 1467 copied_issue_ids = {@copied_from.id => self.id}
1464 1468 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1465 1469 # Do not copy self when copying an issue as a descendant of the copied issue
1466 1470 next if child == self
1467 1471 # Do not copy subtasks of issues that were not copied
1468 1472 next unless copied_issue_ids[child.parent_id]
1469 1473 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1470 1474 unless child.visible?
1471 1475 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1472 1476 next
1473 1477 end
1474 1478 copy = Issue.new.copy_from(child, copy_options)
1475 1479 if @current_journal
1476 1480 copy.init_journal(@current_journal.user)
1477 1481 end
1478 1482 copy.author = author
1479 1483 copy.project = project
1480 1484 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1481 1485 unless copy.save
1482 1486 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
1483 1487 next
1484 1488 end
1485 1489 copied_issue_ids[child.id] = copy.id
1486 1490 end
1487 1491 end
1488 1492 @after_create_from_copy_handled = true
1489 1493 end
1490 1494
1491 1495 def update_nested_set_attributes
1492 1496 if parent_id_changed?
1493 1497 update_nested_set_attributes_on_parent_change
1494 1498 end
1495 1499 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1496 1500 end
1497 1501
1498 1502 # Updates the nested set for when an existing issue is moved
1499 1503 def update_nested_set_attributes_on_parent_change
1500 1504 former_parent_id = parent_id_was
1501 1505 # delete invalid relations of all descendants
1502 1506 self_and_descendants.each do |issue|
1503 1507 issue.relations.each do |relation|
1504 1508 relation.destroy unless relation.valid?
1505 1509 end
1506 1510 end
1507 1511 # update former parent
1508 1512 recalculate_attributes_for(former_parent_id) if former_parent_id
1509 1513 end
1510 1514
1511 1515 def update_parent_attributes
1512 1516 if parent_id
1513 1517 recalculate_attributes_for(parent_id)
1514 1518 association(:parent).reset
1515 1519 end
1516 1520 end
1517 1521
1518 1522 def recalculate_attributes_for(issue_id)
1519 1523 if issue_id && p = Issue.find_by_id(issue_id)
1520 1524 if p.priority_derived?
1521 1525 # priority = highest priority of open children
1522 1526 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1523 1527 p.priority = IssuePriority.find_by_position(priority_position)
1524 1528 else
1525 1529 p.priority = IssuePriority.default
1526 1530 end
1527 1531 end
1528 1532
1529 1533 if p.dates_derived?
1530 1534 # start/due dates = lowest/highest dates of children
1531 1535 p.start_date = p.children.minimum(:start_date)
1532 1536 p.due_date = p.children.maximum(:due_date)
1533 1537 if p.start_date && p.due_date && p.due_date < p.start_date
1534 1538 p.start_date, p.due_date = p.due_date, p.start_date
1535 1539 end
1536 1540 end
1537 1541
1538 1542 if p.done_ratio_derived?
1539 1543 # done ratio = weighted average ratio of leaves
1540 1544 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1541 1545 child_count = p.children.count
1542 1546 if child_count > 0
1543 1547 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1544 1548 if average == 0
1545 1549 average = 1
1546 1550 end
1547 1551 done = p.children.joins(:status).
1548 1552 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1549 1553 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1550 1554 progress = done / (average * child_count)
1551 1555 p.done_ratio = progress.round
1552 1556 end
1553 1557 end
1554 1558 end
1555 1559
1556 1560 # ancestors will be recursively updated
1557 1561 p.save(:validate => false)
1558 1562 end
1559 1563 end
1560 1564
1561 1565 # Update issues so their versions are not pointing to a
1562 1566 # fixed_version that is not shared with the issue's project
1563 1567 def self.update_versions(conditions=nil)
1564 1568 # Only need to update issues with a fixed_version from
1565 1569 # a different project and that is not systemwide shared
1566 1570 Issue.joins(:project, :fixed_version).
1567 1571 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1568 1572 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1569 1573 " AND #{Version.table_name}.sharing <> 'system'").
1570 1574 where(conditions).each do |issue|
1571 1575 next if issue.project.nil? || issue.fixed_version.nil?
1572 1576 unless issue.project.shared_versions.include?(issue.fixed_version)
1573 1577 issue.init_journal(User.current)
1574 1578 issue.fixed_version = nil
1575 1579 issue.save
1576 1580 end
1577 1581 end
1578 1582 end
1579 1583
1580 1584 # Callback on file attachment
1581 1585 def attachment_added(attachment)
1582 1586 if current_journal && !attachment.new_record?
1583 1587 current_journal.journalize_attachment(attachment, :added)
1584 1588 end
1585 1589 end
1586 1590
1587 1591 # Callback on attachment deletion
1588 1592 def attachment_removed(attachment)
1589 1593 if current_journal && !attachment.new_record?
1590 1594 current_journal.journalize_attachment(attachment, :removed)
1591 1595 current_journal.save
1592 1596 end
1593 1597 end
1594 1598
1595 1599 # Called after a relation is added
1596 1600 def relation_added(relation)
1597 1601 if current_journal
1598 1602 current_journal.journalize_relation(relation, :added)
1599 1603 current_journal.save
1600 1604 end
1601 1605 end
1602 1606
1603 1607 # Called after a relation is removed
1604 1608 def relation_removed(relation)
1605 1609 if current_journal
1606 1610 current_journal.journalize_relation(relation, :removed)
1607 1611 current_journal.save
1608 1612 end
1609 1613 end
1610 1614
1611 1615 # Default assignment based on category
1612 1616 def default_assign
1613 1617 if assigned_to.nil? && category && category.assigned_to
1614 1618 self.assigned_to = category.assigned_to
1615 1619 end
1616 1620 end
1617 1621
1618 1622 # Updates start/due dates of following issues
1619 1623 def reschedule_following_issues
1620 1624 if start_date_changed? || due_date_changed?
1621 1625 relations_from.each do |relation|
1622 1626 relation.set_issue_to_dates
1623 1627 end
1624 1628 end
1625 1629 end
1626 1630
1627 1631 # Closes duplicates if the issue is being closed
1628 1632 def close_duplicates
1629 1633 if closing?
1630 1634 duplicates.each do |duplicate|
1631 1635 # Reload is needed in case the duplicate was updated by a previous duplicate
1632 1636 duplicate.reload
1633 1637 # Don't re-close it if it's already closed
1634 1638 next if duplicate.closed?
1635 1639 # Same user and notes
1636 1640 if @current_journal
1637 1641 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1638 1642 duplicate.private_notes = @current_journal.private_notes
1639 1643 end
1640 1644 duplicate.update_attribute :status, self.status
1641 1645 end
1642 1646 end
1643 1647 end
1644 1648
1645 1649 # Make sure updated_on is updated when adding a note and set updated_on now
1646 1650 # so we can set closed_on with the same value on closing
1647 1651 def force_updated_on_change
1648 1652 if @current_journal || changed?
1649 1653 self.updated_on = current_time_from_proper_timezone
1650 1654 if new_record?
1651 1655 self.created_on = updated_on
1652 1656 end
1653 1657 end
1654 1658 end
1655 1659
1656 1660 # Callback for setting closed_on when the issue is closed.
1657 1661 # The closed_on attribute stores the time of the last closing
1658 1662 # and is preserved when the issue is reopened.
1659 1663 def update_closed_on
1660 1664 if closing?
1661 1665 self.closed_on = updated_on
1662 1666 end
1663 1667 end
1664 1668
1665 1669 # Saves the changes in a Journal
1666 1670 # Called after_save
1667 1671 def create_journal
1668 1672 if current_journal
1669 1673 current_journal.save
1670 1674 end
1671 1675 end
1672 1676
1673 1677 def send_notification
1674 1678 if notify? && Setting.notified_events.include?('issue_added')
1675 1679 Mailer.deliver_issue_add(self)
1676 1680 end
1677 1681 end
1678 1682
1679 1683 # Stores the previous assignee so we can still have access
1680 1684 # to it during after_save callbacks (assigned_to_id_was is reset)
1681 1685 def set_assigned_to_was
1682 1686 @previous_assigned_to_id = assigned_to_id_was
1683 1687 end
1684 1688
1685 1689 # Clears the previous assignee at the end of after_save callbacks
1686 1690 def clear_assigned_to_was
1687 1691 @assigned_to_was = nil
1688 1692 @previous_assigned_to_id = nil
1689 1693 end
1690 1694
1691 1695 def clear_disabled_fields
1692 1696 if tracker
1693 1697 tracker.disabled_core_fields.each do |attribute|
1694 1698 send "#{attribute}=", nil
1695 1699 end
1696 1700 self.done_ratio ||= 0
1697 1701 end
1698 1702 end
1699 1703 end
General Comments 0
You need to be logged in to leave comments. Login now