| 
									
										
										
										
											2019-03-17 00:29:58 +00:00
										 |  |  | # frozen_string_literal: true | 
					
						
							| 
									
										
										
										
											2019-03-15 01:32:57 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-02-27 13:34:41 +00:00
										 |  |  | # Redmine - project management software | 
					
						
							| 
									
										
										
										
											2020-03-03 00:24:10 +00:00
										 |  |  | # Copyright (C) 2006-2020  Jean-Philippe Lang | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  | # | 
					
						
							|  |  |  | # This program is free software; you can redistribute it and/or | 
					
						
							|  |  |  | # modify it under the terms of the GNU General Public License | 
					
						
							|  |  |  | # as published by the Free Software Foundation; either version 2 | 
					
						
							|  |  |  | # of the License, or (at your option) any later version. | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | # | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  | # This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  | # GNU General Public License for more details. | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | # | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  | # You should have received a copy of the GNU General Public License | 
					
						
							|  |  |  | # along with this program; if not, write to the Free Software | 
					
						
							|  |  |  | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Issue < ActiveRecord::Base | 
					
						
							| 
									
										
										
										
											2010-12-12 13:11:53 +00:00
										 |  |  |   include Redmine::SafeAttributes | 
					
						
							| 
									
										
										
										
											2012-10-29 10:06:30 +00:00
										 |  |  |   include Redmine::Utils::DateCalculation | 
					
						
							| 
									
										
										
										
											2013-06-04 17:17:48 +00:00
										 |  |  |   include Redmine::I18n | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |   before_save :set_parent_id | 
					
						
							|  |  |  |   include Redmine::NestedSet::IssueNestedSet | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  |   belongs_to :project | 
					
						
							|  |  |  |   belongs_to :tracker | 
					
						
							| 
									
										
										
										
											2014-10-22 18:41:03 +00:00
										 |  |  |   belongs_to :status, :class_name => 'IssueStatus' | 
					
						
							|  |  |  |   belongs_to :author, :class_name => 'User' | 
					
						
							|  |  |  |   belongs_to :assigned_to, :class_name => 'Principal' | 
					
						
							|  |  |  |   belongs_to :fixed_version, :class_name => 'Version' | 
					
						
							|  |  |  |   belongs_to :priority, :class_name => 'IssuePriority' | 
					
						
							|  |  |  |   belongs_to :category, :class_name => 'IssueCategory' | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-21 08:27:46 +00:00
										 |  |  |   has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized | 
					
						
							| 
									
										
										
										
											2013-12-18 18:39:09 +00:00
										 |  |  |   has_many :time_entries, :dependent => :destroy | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |   has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")} | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-05-05 13:22:27 +00:00
										 |  |  |   has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all | 
					
						
							|  |  |  |   has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-07-24 09:34:23 +00:00
										 |  |  |   acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed | 
					
						
							| 
									
										
										
										
											2008-06-27 20:13:56 +00:00
										 |  |  |   acts_as_customizable | 
					
						
							| 
									
										
										
										
											2007-04-21 12:08:31 +00:00
										 |  |  |   acts_as_watchable | 
					
						
							| 
									
										
										
										
											2015-01-08 22:04:00 +00:00
										 |  |  |   acts_as_searchable :columns => ['subject', "#{table_name}.description"], | 
					
						
							| 
									
										
										
										
											2015-01-10 10:09:34 +00:00
										 |  |  |                      :preload => [:project, :status, :tracker], | 
					
						
							|  |  |  |                      :scope => lambda {|options| options[:open_issues] ? self.open : self.all} | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-08-02 04:36:06 +00:00
										 |  |  |   acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, | 
					
						
							| 
									
										
										
										
											2009-01-11 11:01:35 +00:00
										 |  |  |                 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:53 +00:00
										 |  |  |                 :type => Proc.new {|o| 'issue' + (o.closed? ? '-closed' : '')} | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-17 12:41:33 +00:00
										 |  |  |   acts_as_activity_provider :scope => proc {preload(:project, :author, :tracker, :status)}, | 
					
						
							| 
									
										
										
										
											2008-11-30 11:18:22 +00:00
										 |  |  |                             :author_key => :author_id | 
					
						
							| 
									
										
										
										
											2009-12-11 18:48:34 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   DONE_RATIO_OPTIONS = %w(issue_field issue_status) | 
					
						
							| 
									
										
										
										
											2010-03-04 16:18:51 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-10 03:50:47 +00:00
										 |  |  |   attr_reader :transition_warning | 
					
						
							| 
									
										
										
										
											2019-09-20 15:03:37 +00:00
										 |  |  |   attr_writer :deleted_attachment_ids | 
					
						
							| 
									
										
										
										
											2012-10-03 21:36:19 +00:00
										 |  |  |   delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true | 
					
						
							| 
									
										
										
										
											2010-03-10 05:10:43 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-12-06 11:23:05 +00:00
										 |  |  |   validates_presence_of :subject, :project, :tracker | 
					
						
							|  |  |  |   validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?} | 
					
						
							|  |  |  |   validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?} | 
					
						
							|  |  |  |   validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?} | 
					
						
							| 
									
										
										
										
											2010-03-10 05:10:43 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-07-16 17:16:49 +00:00
										 |  |  |   validates_length_of :subject, :maximum => 255
 | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  |   validates_inclusion_of :done_ratio, :in => 0..100
 | 
					
						
							| 
									
										
										
										
											2013-01-05 16:34:41 +00:00
										 |  |  |   validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid} | 
					
						
							| 
									
										
										
										
											2013-01-05 16:09:15 +00:00
										 |  |  |   validates :start_date, :date => true | 
					
						
							|  |  |  |   validates :due_date, :date => true | 
					
						
							| 
									
										
										
										
											2017-05-27 08:34:41 +00:00
										 |  |  |   validate :validate_issue, :validate_required_fields, :validate_permissions | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   scope :visible, (lambda do |*args| | 
					
						
							| 
									
										
										
										
											2015-01-17 17:02:55 +00:00
										 |  |  |     joins(:project). | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |     where(Issue.visible_condition(args.shift || User.current, *args)) | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   end) | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   scope :open, (lambda do |*args| | 
					
						
							| 
									
										
										
										
											2019-09-24 15:56:51 +00:00
										 |  |  |     is_closed = !args.empty? ? !args.first : false | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |     joins(:status). | 
					
						
							| 
									
										
										
										
											2017-01-21 10:09:13 +00:00
										 |  |  |     where(:issue_statuses => {:is_closed => is_closed}) | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   end) | 
					
						
							| 
									
										
										
										
											2009-12-11 18:48:34 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:53 +00:00
										 |  |  |   scope :recently_updated, lambda {order(:updated_on => :desc)} | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   scope :on_active_project, (lambda do | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |     joins(:project). | 
					
						
							| 
									
										
										
										
											2017-01-21 10:09:13 +00:00
										 |  |  |     where(:projects => {:status => Project::STATUS_ACTIVE}) | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   end) | 
					
						
							|  |  |  |   scope :fixed_version, (lambda do |versions| | 
					
						
							| 
									
										
										
										
											2013-01-06 14:03:49 +00:00
										 |  |  |     ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v} | 
					
						
							| 
									
										
										
										
											2017-01-21 10:10:24 +00:00
										 |  |  |     ids.any? ? where(:fixed_version_id => ids) : none | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   end) | 
					
						
							|  |  |  |   scope :assigned_to, (lambda do |arg| | 
					
						
							| 
									
										
										
										
											2015-10-21 17:00:14 +00:00
										 |  |  |     arg = Array(arg).uniq | 
					
						
							|  |  |  |     ids = arg.map {|p| p.is_a?(Principal) ? p.id : p} | 
					
						
							|  |  |  |     ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq | 
					
						
							|  |  |  |     ids.compact! | 
					
						
							|  |  |  |     ids.any? ? where(:assigned_to_id => ids) : none | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   end) | 
					
						
							|  |  |  |   scope :like, (lambda do |q| | 
					
						
							| 
									
										
										
										
											2017-01-21 10:05:53 +00:00
										 |  |  |     q = q.to_s | 
					
						
							|  |  |  |     if q.present? | 
					
						
							|  |  |  |       where("LOWER(#{table_name}.subject) LIKE LOWER(?)", "%#{q}%") | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2020-11-12 12:31:20 +00:00
										 |  |  |   end) | 
					
						
							| 
									
										
										
										
											2010-09-10 03:09:02 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-06 21:11:37 +00:00
										 |  |  |   before_validation :default_assign, on: :create | 
					
						
							| 
									
										
										
										
											2015-04-14 17:16:52 +00:00
										 |  |  |   before_validation :clear_disabled_fields | 
					
						
							| 
									
										
										
										
											2013-10-24 05:40:21 +00:00
										 |  |  |   before_save :close_duplicates, :update_done_ratio_from_issue_status, | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |               :force_updated_on_change, :update_closed_on | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |   after_save do |issue| | 
					
						
							|  |  |  |     if !issue.saved_change_to_id? && issue.saved_change_to_project_id? | 
					
						
							|  |  |  |       issue.send :after_project_change | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2013-10-24 05:40:21 +00:00
										 |  |  |   after_save :reschedule_following_issues, :update_nested_set_attributes, | 
					
						
							| 
									
										
										
										
											2016-07-13 20:04:14 +00:00
										 |  |  |              :update_parent_attributes, :delete_selected_attachments, :create_journal | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |   # Should be after_create but would be called before previous after_save callbacks | 
					
						
							|  |  |  |   after_save :after_create_from_copy | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   after_destroy :update_parent_attributes | 
					
						
							| 
									
										
										
										
											2018-10-10 17:13:09 +00:00
										 |  |  |   after_create_commit :send_notification | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-03-15 16:00:39 +00:00
										 |  |  |   # Returns a SQL conditions string used to find all issues visible by the specified user | 
					
						
							|  |  |  |   def self.visible_condition(user, options={}) | 
					
						
							| 
									
										
										
										
											2011-04-11 17:53:15 +00:00
										 |  |  |     Project.allowed_to_condition(user, :view_issues, options) do |role, user| | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:26 +00:00
										 |  |  |       sql = | 
					
						
							|  |  |  |         if user.id && user.logged? | 
					
						
							|  |  |  |           case role.issues_visibility | 
					
						
							|  |  |  |           when 'all' | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:29 +00:00
										 |  |  |             '1=1' | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:26 +00:00
										 |  |  |           when 'default' | 
					
						
							|  |  |  |             user_ids = [user.id] + user.groups.pluck(:id).compact | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |             "(#{table_name}.is_private = #{connection.quoted_false} " \ | 
					
						
							|  |  |  |               "OR #{table_name}.author_id = #{user.id} " \ | 
					
						
							|  |  |  |               "OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:26 +00:00
										 |  |  |           when 'own' | 
					
						
							|  |  |  |             user_ids = [user.id] + user.groups.pluck(:id).compact | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |             "(#{table_name}.author_id = #{user.id} OR " \ | 
					
						
							|  |  |  |               "#{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:26 +00:00
										 |  |  |           else | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:29 +00:00
										 |  |  |             '1=0' | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:26 +00:00
										 |  |  |           end | 
					
						
							| 
									
										
										
										
											2012-09-19 21:48:33 +00:00
										 |  |  |         else | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:26 +00:00
										 |  |  |           "(#{table_name}.is_private = #{connection.quoted_false})" | 
					
						
							| 
									
										
										
										
											2012-09-19 21:48:33 +00:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-06-05 11:50:09 +00:00
										 |  |  |       unless role.permissions_all_trackers?(:view_issues) | 
					
						
							|  |  |  |         tracker_ids = role.permissions_tracker_ids(:view_issues) | 
					
						
							|  |  |  |         if tracker_ids.any? | 
					
						
							|  |  |  |           sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))" | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |           sql = '1=0' | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       sql | 
					
						
							| 
									
										
										
										
											2011-04-11 17:53:15 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2011-03-15 16:00:39 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-01-31 13:22:29 +00:00
										 |  |  |   # Returns true if usr or current user is allowed to view the issue | 
					
						
							|  |  |  |   def visible?(usr=nil) | 
					
						
							| 
									
										
										
										
											2011-04-11 17:53:15 +00:00
										 |  |  |     (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:36 +00:00
										 |  |  |       visible = | 
					
						
							|  |  |  |         if user.logged? | 
					
						
							|  |  |  |           case role.issues_visibility | 
					
						
							|  |  |  |           when 'all' | 
					
						
							|  |  |  |             true | 
					
						
							|  |  |  |           when 'default' | 
					
						
							|  |  |  |             !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to)) | 
					
						
							|  |  |  |           when 'own' | 
					
						
							|  |  |  |             self.author == user || user.is_or_belongs_to?(assigned_to) | 
					
						
							|  |  |  |           else | 
					
						
							|  |  |  |             false | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2012-09-20 19:26:58 +00:00
										 |  |  |         else | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:36 +00:00
										 |  |  |           !self.is_private? | 
					
						
							| 
									
										
										
										
											2012-09-20 19:26:58 +00:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-06-05 11:50:09 +00:00
										 |  |  |       unless role.permissions_all_trackers?(:view_issues) | 
					
						
							|  |  |  |         visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       visible | 
					
						
							| 
									
										
										
										
											2011-04-11 17:53:15 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2009-01-31 13:22:29 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-06-05 13:45:10 +00:00
										 |  |  |   # Returns true if user or current user is allowed to edit or add notes to the issue | 
					
						
							| 
									
										
										
										
											2012-12-22 10:30:42 +00:00
										 |  |  |   def editable?(user=User.current) | 
					
						
							| 
									
										
										
										
											2016-06-05 13:45:10 +00:00
										 |  |  |     attributes_editable?(user) || notes_addable?(user) | 
					
						
							| 
									
										
										
										
											2015-02-13 19:30:15 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns true if user or current user is allowed to edit the issue | 
					
						
							|  |  |  |   def attributes_editable?(user=User.current) | 
					
						
							| 
									
										
										
										
											2019-04-25 05:32:58 +00:00
										 |  |  |     user_tracker_permission?(user, :edit_issues) || ( | 
					
						
							|  |  |  |       user_tracker_permission?(user, :edit_own_issues) && author == user | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2016-06-05 13:45:10 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-06-06 06:41:06 +00:00
										 |  |  |   # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable? | 
					
						
							|  |  |  |   def attachments_editable?(user=User.current) | 
					
						
							|  |  |  |     attributes_editable?(user) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-06-05 13:45:10 +00:00
										 |  |  |   # Returns true if user or current user is allowed to add notes to the issue | 
					
						
							|  |  |  |   def notes_addable?(user=User.current) | 
					
						
							|  |  |  |     user_tracker_permission?(user, :add_issue_notes) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns true if user or current user is allowed to delete the issue | 
					
						
							|  |  |  |   def deletable?(user=User.current) | 
					
						
							|  |  |  |     user_tracker_permission?(user, :delete_issues) | 
					
						
							| 
									
										
										
										
											2012-12-22 10:30:42 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-12-18 14:57:58 +00:00
										 |  |  |   def initialize(attributes=nil, *args) | 
					
						
							|  |  |  |     super | 
					
						
							| 
									
										
										
										
											2007-10-05 17:44:15 +00:00
										 |  |  |     if new_record? | 
					
						
							|  |  |  |       # set default values for new records only | 
					
						
							| 
									
										
										
										
											2009-05-30 23:30:36 +00:00
										 |  |  |       self.priority ||= IssuePriority.default | 
					
						
							| 
									
										
										
										
											2012-01-08 12:26:57 +00:00
										 |  |  |       self.watcher_user_ids = [] | 
					
						
							| 
									
										
										
										
											2007-10-05 17:44:15 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |   def create_or_update(*args) | 
					
						
							| 
									
										
										
										
											2013-02-17 09:30:17 +00:00
										 |  |  |     super | 
					
						
							|  |  |  |   ensure | 
					
						
							|  |  |  |     @status_was = nil | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  |   private :create_or_update | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-06-18 17:16:13 +00:00
										 |  |  |   # AR#Persistence#destroy would raise and RecordNotFound exception | 
					
						
							|  |  |  |   # if the issue was already deleted or updated (non matching lock_version). | 
					
						
							|  |  |  |   # This is a problem when bulk deleting issues or deleting a project | 
					
						
							|  |  |  |   # (because an issue may already be deleted if its parent was deleted | 
					
						
							|  |  |  |   # first). | 
					
						
							|  |  |  |   # The issue is reloaded by the nested_set before being deleted so | 
					
						
							|  |  |  |   # the lock_version condition should not be an issue but we handle it. | 
					
						
							|  |  |  |   def destroy | 
					
						
							|  |  |  |     super | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |   rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound | 
					
						
							| 
									
										
										
										
											2012-06-18 17:16:13 +00:00
										 |  |  |     # Stale or already deleted | 
					
						
							|  |  |  |     begin | 
					
						
							|  |  |  |       reload | 
					
						
							|  |  |  |     rescue ActiveRecord::RecordNotFound | 
					
						
							|  |  |  |       # The issue was actually already deleted | 
					
						
							|  |  |  |       @destroyed = true | 
					
						
							|  |  |  |       return freeze | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     # The issue was stale, retry to destroy | 
					
						
							|  |  |  |     super | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-02-28 18:47:25 +00:00
										 |  |  |   alias :base_reload :reload | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   def reload(*args) | 
					
						
							|  |  |  |     @workflow_rule_by_attribute = nil | 
					
						
							| 
									
										
										
										
											2012-07-27 19:36:53 +00:00
										 |  |  |     @assignable_versions = nil | 
					
						
							| 
									
										
										
										
											2013-02-24 09:59:45 +00:00
										 |  |  |     @relations = nil | 
					
						
							| 
									
										
										
										
											2014-11-22 09:39:10 +00:00
										 |  |  |     @spent_hours = nil | 
					
						
							| 
									
										
										
										
											2015-05-25 12:09:01 +00:00
										 |  |  |     @total_spent_hours = nil | 
					
						
							| 
									
										
										
										
											2015-05-25 11:37:12 +00:00
										 |  |  |     @total_estimated_hours = nil | 
					
						
							| 
									
										
										
										
											2017-03-05 07:42:52 +00:00
										 |  |  |     @last_updated_by = nil | 
					
						
							| 
									
										
										
										
											2017-03-05 07:58:07 +00:00
										 |  |  |     @last_notes = nil | 
					
						
							| 
									
										
										
										
											2013-02-28 18:47:25 +00:00
										 |  |  |     base_reload(*args) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-06-27 20:13:56 +00:00
										 |  |  |   # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields | 
					
						
							|  |  |  |   def available_custom_fields | 
					
						
							| 
									
										
										
										
											2014-02-22 11:23:39 +00:00
										 |  |  |     (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : [] | 
					
						
							| 
									
										
										
										
											2008-06-27 20:13:56 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-07-13 09:20:11 +00:00
										 |  |  |   def visible_custom_field_values(user=nil) | 
					
						
							|  |  |  |     user_real = user || User.current | 
					
						
							|  |  |  |     custom_field_values.select do |value| | 
					
						
							|  |  |  |       value.custom_field.visible_by?(project, user_real) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-23 08:25:25 +00:00
										 |  |  |   # Overrides Redmine::Acts::Customizable::InstanceMethods#set_custom_field_default? | 
					
						
							|  |  |  |   def set_custom_field_default?(custom_value) | 
					
						
							|  |  |  |     new_record? || project_id_changed?|| tracker_id_changed? | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-08 11:16:54 +00:00
										 |  |  |   # Copies attributes from another issue, arg can be an id or an Issue | 
					
						
							| 
									
										
										
										
											2012-01-20 18:22:43 +00:00
										 |  |  |   def copy_from(arg, options={}) | 
					
						
							| 
									
										
										
										
											2010-04-29 23:26:52 +00:00
										 |  |  |     issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |     self.attributes = | 
					
						
							|  |  |  |       issue.attributes.dup.except( | 
					
						
							|  |  |  |         "id", "root_id", "parent_id", "lft", "rgt", | 
					
						
							|  |  |  |         "created_on", "updated_on", "status_id", "closed_on" | 
					
						
							|  |  |  |       ) | 
					
						
							|  |  |  |     self.custom_field_values = | 
					
						
							|  |  |  |       issue.custom_field_values.inject({}) do |h, v| | 
					
						
							|  |  |  |         h[v.custom_field_id] = v.value | 
					
						
							|  |  |  |         h | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2017-04-03 10:30:29 +00:00
										 |  |  |     if options[:keep_status] | 
					
						
							|  |  |  |       self.status = issue.status | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2012-01-06 20:06:25 +00:00
										 |  |  |     self.author = User.current | 
					
						
							| 
									
										
										
										
											2012-01-20 18:22:43 +00:00
										 |  |  |     unless options[:attachments] == false | 
					
						
							| 
									
										
										
										
											2014-04-08 11:22:33 +00:00
										 |  |  |       self.attachments = issue.attachments.map do |attachement| | 
					
						
							| 
									
										
										
										
											2012-01-20 18:22:43 +00:00
										 |  |  |         attachement.copy(:container => self) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2012-01-20 17:56:28 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2017-04-06 17:34:45 +00:00
										 |  |  |     unless options[:watchers] == false | 
					
						
							| 
									
										
										
										
											2018-05-19 00:44:10 +00:00
										 |  |  |       self.watcher_user_ids = | 
					
						
							|  |  |  |         issue.watcher_users.select{|u| u.status == User::STATUS_ACTIVE}.map(&:id) | 
					
						
							| 
									
										
										
										
											2017-04-06 17:34:45 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2012-01-08 11:16:54 +00:00
										 |  |  |     @copied_from = issue | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |     @copy_options = options | 
					
						
							| 
									
										
										
										
											2007-10-28 14:31:59 +00:00
										 |  |  |     self | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-07 18:02:02 +00:00
										 |  |  |   # Returns an unsaved copy of the issue | 
					
						
							| 
									
										
										
										
											2012-04-14 06:21:03 +00:00
										 |  |  |   def copy(attributes=nil, copy_options={}) | 
					
						
							|  |  |  |     copy = self.class.new.copy_from(self, copy_options) | 
					
						
							| 
									
										
										
										
											2012-01-07 18:02:02 +00:00
										 |  |  |     copy.attributes = attributes if attributes | 
					
						
							|  |  |  |     copy | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-08 11:16:54 +00:00
										 |  |  |   # Returns true if the issue is a copy | 
					
						
							|  |  |  |   def copy? | 
					
						
							|  |  |  |     @copied_from.present? | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |   def status_id=(status_id) | 
					
						
							|  |  |  |     if status_id.to_s != self.status_id.to_s | 
					
						
							|  |  |  |       self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     self.status_id | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Sets the status. | 
					
						
							| 
									
										
										
										
											2014-12-05 09:05:03 +00:00
										 |  |  |   def status=(status) | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |     if status != self.status | 
					
						
							|  |  |  |       @workflow_rule_by_attribute = nil | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     association(:status).writer(status) | 
					
						
							| 
									
										
										
										
											2010-03-20 17:37:04 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-10-05 17:44:15 +00:00
										 |  |  |   def priority_id=(pid) | 
					
						
							|  |  |  |     self.priority = nil | 
					
						
							|  |  |  |     write_attribute(:priority_id, pid) | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2009-11-29 19:46:40 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-07 12:34:52 +00:00
										 |  |  |   def category_id=(cid) | 
					
						
							|  |  |  |     self.category = nil | 
					
						
							|  |  |  |     write_attribute(:category_id, cid) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def fixed_version_id=(vid) | 
					
						
							|  |  |  |     self.fixed_version = nil | 
					
						
							|  |  |  |     write_attribute(:fixed_version_id, vid) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-10-26 14:44:11 +00:00
										 |  |  |   def tracker_id=(tracker_id) | 
					
						
							|  |  |  |     if tracker_id.to_s != self.tracker_id.to_s | 
					
						
							|  |  |  |       self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     self.tracker_id | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |   # Sets the tracker. | 
					
						
							|  |  |  |   # This will set the status to the default status of the new tracker if: | 
					
						
							|  |  |  |   # * the status was the default for the previous tracker | 
					
						
							|  |  |  |   # * or if the status was not part of the new tracker statuses | 
					
						
							|  |  |  |   # * or the status was nil | 
					
						
							| 
									
										
										
										
											2014-10-26 14:44:11 +00:00
										 |  |  |   def tracker=(tracker) | 
					
						
							| 
									
										
										
										
											2016-04-11 17:11:59 +00:00
										 |  |  |     tracker_was = self.tracker | 
					
						
							| 
									
										
										
										
											2016-04-11 17:17:29 +00:00
										 |  |  |     association(:tracker).writer(tracker) | 
					
						
							| 
									
										
										
										
											2016-04-11 17:11:59 +00:00
										 |  |  |     if tracker != tracker_was | 
					
						
							| 
									
										
										
										
											2016-04-11 17:17:29 +00:00
										 |  |  |       if status == tracker_was.try(:default_status) | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |         self.status = nil | 
					
						
							|  |  |  |       elsif status && tracker && !tracker.issue_status_ids.include?(status.id) | 
					
						
							|  |  |  |         self.status = nil | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-04-11 17:11:59 +00:00
										 |  |  |       reassign_custom_field_values | 
					
						
							| 
									
										
										
										
											2016-04-11 17:17:29 +00:00
										 |  |  |       @workflow_rule_by_attribute = nil | 
					
						
							| 
									
										
										
										
											2016-04-11 17:11:59 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |     self.status ||= default_status | 
					
						
							|  |  |  |     self.tracker | 
					
						
							| 
									
										
										
										
											2009-11-29 19:46:40 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-08-21 08:46:10 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |   def project_id=(project_id) | 
					
						
							|  |  |  |     if project_id.to_s != self.project_id.to_s | 
					
						
							|  |  |  |       self.project = (project_id.present? ? Project.find_by_id(project_id) : nil) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2014-10-26 14:44:11 +00:00
										 |  |  |     self.project_id | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |   # Sets the project. | 
					
						
							|  |  |  |   # Unless keep_tracker argument is set to true, this will change the tracker | 
					
						
							|  |  |  |   # to the first tracker of the new project if the previous tracker is not part | 
					
						
							|  |  |  |   # of the new project trackers. | 
					
						
							| 
									
										
										
										
											2015-11-01 08:11:36 +00:00
										 |  |  |   # This will: | 
					
						
							|  |  |  |   # * clear the fixed_version is it's no longer valid for the new project. | 
					
						
							|  |  |  |   # * clear the parent issue if it's no longer valid for the new project. | 
					
						
							|  |  |  |   # * set the category to the category with the same name in the new | 
					
						
							|  |  |  |   #   project if it exists, or clear it if it doesn't. | 
					
						
							|  |  |  |   # * for new issue, set the fixed_version to the project default version | 
					
						
							|  |  |  |   #   if it's a valid fixed_version. | 
					
						
							| 
									
										
										
										
											2012-01-07 12:34:52 +00:00
										 |  |  |   def project=(project, keep_tracker=false) | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |     project_was = self.project | 
					
						
							| 
									
										
										
										
											2014-10-26 14:44:11 +00:00
										 |  |  |     association(:project).writer(project) | 
					
						
							| 
									
										
										
										
											2017-04-06 19:57:00 +00:00
										 |  |  |     if project != project_was | 
					
						
							|  |  |  |       @safe_attribute_names = nil | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |     if project_was && project && project_was != project | 
					
						
							| 
									
										
										
										
											2012-07-27 19:36:53 +00:00
										 |  |  |       @assignable_versions = nil | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-07 12:34:52 +00:00
										 |  |  |       unless keep_tracker || project.trackers.include?(tracker) | 
					
						
							|  |  |  |         self.tracker = project.trackers.first | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |       # Reassign to the category with same name if any | 
					
						
							|  |  |  |       if category | 
					
						
							|  |  |  |         self.category = project.issue_categories.find_by_name(category.name) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-12-10 12:02:37 +00:00
										 |  |  |       # Clear the assignee if not available in the new project for new issues (eg. copy) | 
					
						
							|  |  |  |       # For existing issue, the previous assignee is still valid, so we keep it | 
					
						
							|  |  |  |       if new_record? && assigned_to && !assignable_users.include?(assigned_to) | 
					
						
							|  |  |  |         self.assigned_to_id = nil | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |       # Keep the fixed_version if it's still valid in the new_project | 
					
						
							|  |  |  |       if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version) | 
					
						
							|  |  |  |         self.fixed_version = nil | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2012-10-10 17:38:17 +00:00
										 |  |  |       # Clear the parent task if it's no longer valid | 
					
						
							|  |  |  |       unless valid_parent_project? | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |         self.parent_issue_id = nil | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-04-11 17:11:59 +00:00
										 |  |  |       reassign_custom_field_values | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |       @workflow_rule_by_attribute = nil | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2015-11-01 08:11:36 +00:00
										 |  |  |     # Set fixed_version to the project default version if it's valid | 
					
						
							|  |  |  |     if new_record? && fixed_version.nil? && project && project.default_version_id? | 
					
						
							|  |  |  |       if project.shared_versions.open.exists?(project.default_version_id) | 
					
						
							|  |  |  |         self.fixed_version_id = project.default_version_id | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |     self.project | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-07-02 11:42:46 +00:00
										 |  |  |   def description=(arg) | 
					
						
							|  |  |  |     if arg.is_a?(String) | 
					
						
							|  |  |  |       arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n") | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     write_attribute(:description, arg) | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-07-13 20:04:14 +00:00
										 |  |  |   def deleted_attachment_ids | 
					
						
							|  |  |  |     Array(@deleted_attachment_ids).map(&:to_i) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-04-25 17:17:49 +00:00
										 |  |  |   # Overrides assign_attributes so that project and tracker get assigned first | 
					
						
							| 
									
										
										
										
											2016-07-14 07:34:12 +00:00
										 |  |  |   def assign_attributes(new_attributes, *args) | 
					
						
							| 
									
										
										
										
											2009-12-09 09:12:29 +00:00
										 |  |  |     return if new_attributes.nil? | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-12-08 20:37:12 +00:00
										 |  |  |     attrs = new_attributes.dup | 
					
						
							|  |  |  |     attrs.stringify_keys! | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     %w(project project_id tracker tracker_id).each do |attr| | 
					
						
							|  |  |  |       if attrs.has_key?(attr) | 
					
						
							|  |  |  |         send "#{attr}=", attrs.delete(attr) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2009-12-09 09:12:29 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-07-14 07:34:12 +00:00
										 |  |  |     super attrs, *args | 
					
						
							| 
									
										
										
										
											2009-12-09 09:12:29 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |   def attributes=(new_attributes) | 
					
						
							|  |  |  |     assign_attributes new_attributes | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-04-26 11:59:51 +00:00
										 |  |  |   def estimated_hours=(h) | 
					
						
							| 
									
										
										
										
											2018-01-07 21:41:15 +00:00
										 |  |  |     write_attribute :estimated_hours, (h.is_a?(String) ? (h.to_hours || h) : h) | 
					
						
							| 
									
										
										
										
											2008-04-26 11:59:51 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-08 00:46:24 +00:00
										 |  |  |   safe_attributes( | 
					
						
							|  |  |  |     'project_id', | 
					
						
							| 
									
										
										
										
											2015-02-14 08:03:51 +00:00
										 |  |  |     'tracker_id', | 
					
						
							| 
									
										
										
										
											2010-12-12 13:11:53 +00:00
										 |  |  |     'status_id', | 
					
						
							|  |  |  |     'category_id', | 
					
						
							|  |  |  |     'assigned_to_id', | 
					
						
							|  |  |  |     'priority_id', | 
					
						
							|  |  |  |     'fixed_version_id', | 
					
						
							|  |  |  |     'subject', | 
					
						
							|  |  |  |     'description', | 
					
						
							|  |  |  |     'start_date', | 
					
						
							|  |  |  |     'due_date', | 
					
						
							|  |  |  |     'done_ratio', | 
					
						
							|  |  |  |     'estimated_hours', | 
					
						
							|  |  |  |     'custom_field_values', | 
					
						
							|  |  |  |     'custom_fields', | 
					
						
							|  |  |  |     'lock_version', | 
					
						
							| 
									
										
										
										
											2019-11-08 00:46:24 +00:00
										 |  |  |     :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user)}) | 
					
						
							|  |  |  |   safe_attributes( | 
					
						
							|  |  |  |     'notes', | 
					
						
							|  |  |  |     :if => lambda {|issue, user| issue.notes_addable?(user)}) | 
					
						
							|  |  |  |   safe_attributes( | 
					
						
							|  |  |  |     'private_notes', | 
					
						
							|  |  |  |     :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}) | 
					
						
							|  |  |  |   safe_attributes( | 
					
						
							|  |  |  |     'watcher_user_ids', | 
					
						
							|  |  |  |     :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}) | 
					
						
							|  |  |  |   safe_attributes( | 
					
						
							|  |  |  |     'is_private', | 
					
						
							| 
									
										
										
										
											2020-11-07 12:30:38 +00:00
										 |  |  |     :if => lambda do |issue, user| | 
					
						
							| 
									
										
										
										
											2011-04-15 13:23:13 +00:00
										 |  |  |       user.allowed_to?(:set_issues_private, issue.project) || | 
					
						
							| 
									
										
										
										
											2014-11-08 11:09:42 +00:00
										 |  |  |         (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project)) | 
					
						
							| 
									
										
										
										
											2020-11-07 12:30:38 +00:00
										 |  |  |     end) | 
					
						
							| 
									
										
										
										
											2019-11-08 00:46:24 +00:00
										 |  |  |   safe_attributes( | 
					
						
							|  |  |  |     'parent_issue_id', | 
					
						
							| 
									
										
										
										
											2020-11-07 12:30:38 +00:00
										 |  |  |     :if => lambda do |issue, user| | 
					
						
							| 
									
										
										
										
											2019-09-30 08:59:26 +00:00
										 |  |  |       (issue.new_record? || issue.attributes_editable?(user)) && | 
					
						
							|  |  |  |         user.allowed_to?(:manage_subtasks, issue.project) | 
					
						
							| 
									
										
										
										
											2020-11-07 12:30:38 +00:00
										 |  |  |     end) | 
					
						
							| 
									
										
										
										
											2019-11-08 00:46:24 +00:00
										 |  |  |   safe_attributes( | 
					
						
							|  |  |  |     'deleted_attachment_ids', | 
					
						
							|  |  |  |     :if => lambda {|issue, user| issue.attachments_deletable?(user)}) | 
					
						
							| 
									
										
										
										
											2016-07-13 20:04:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   def safe_attribute_names(user=nil) | 
					
						
							|  |  |  |     names = super | 
					
						
							| 
									
										
										
										
											2012-07-05 12:20:07 +00:00
										 |  |  |     names -= disabled_core_fields | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     names -= read_only_attribute_names(user) | 
					
						
							| 
									
										
										
										
											2015-02-14 08:03:51 +00:00
										 |  |  |     if new_record? | 
					
						
							| 
									
										
										
										
											2015-06-21 16:38:29 +00:00
										 |  |  |       # Make sure that project_id can always be set for new issues | 
					
						
							| 
									
										
										
										
											2015-02-08 12:07:00 +00:00
										 |  |  |       names |= %w(project_id) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |     if dates_derived? | 
					
						
							|  |  |  |       names -= %w(start_date due_date) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     if priority_derived? | 
					
						
							|  |  |  |       names -= %w(priority_id) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2015-05-25 10:03:42 +00:00
										 |  |  |     if done_ratio_derived? | 
					
						
							|  |  |  |       names -= %w(done_ratio) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2012-07-05 12:20:07 +00:00
										 |  |  |     names | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-01-12 20:17:20 +00:00
										 |  |  |   # Safely sets attributes | 
					
						
							|  |  |  |   # Should be called from controllers instead of #attributes= | 
					
						
							|  |  |  |   # attr_accessible is too rough because we still want things like | 
					
						
							|  |  |  |   # Issue.new(:project => foo) to work | 
					
						
							|  |  |  |   def safe_attributes=(attrs, user=User.current) | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |     if attrs.respond_to?(:to_unsafe_hash) | 
					
						
							|  |  |  |       attrs = attrs.to_unsafe_hash | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-27 08:34:41 +00:00
										 |  |  |     @attributes_set_by = user | 
					
						
							| 
									
										
										
										
											2010-11-11 16:37:16 +00:00
										 |  |  |     return unless attrs.is_a?(Hash) | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |     attrs = attrs.deep_dup | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-07 12:34:52 +00:00
										 |  |  |     # Project and Tracker must be set before since new_statuses_allowed_to depends on it. | 
					
						
							| 
									
										
										
										
											2012-07-05 12:20:07 +00:00
										 |  |  |     if (p = attrs.delete('project_id')) && safe_attribute?('project_id') | 
					
						
							| 
									
										
										
										
											2019-03-27 02:15:24 +00:00
										 |  |  |       if p.is_a?(String) && !/^\d*$/.match?(p) | 
					
						
							| 
									
										
										
										
											2016-10-01 09:06:17 +00:00
										 |  |  |         p_id = Project.find_by_identifier(p).try(:id) | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         p_id = p.to_i | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       if allowed_target_projects(user).where(:id => p_id).exists? | 
					
						
							|  |  |  |         self.project_id = p_id | 
					
						
							| 
									
										
										
										
											2012-01-08 11:48:36 +00:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2015-10-20 18:57:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-17 06:22:35 +00:00
										 |  |  |       if project_id_changed? && attrs['category_id'].present? && attrs['category_id'].to_s == category_id_was.to_s | 
					
						
							| 
									
										
										
										
											2015-10-20 18:57:44 +00:00
										 |  |  |         # Discard submitted category on previous project | 
					
						
							|  |  |  |         attrs.delete('category_id') | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2012-01-07 12:34:52 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2012-01-08 11:48:36 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-05 12:20:07 +00:00
										 |  |  |     if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id') | 
					
						
							| 
									
										
										
										
											2016-05-30 18:20:13 +00:00
										 |  |  |       if allowed_target_trackers(user).where(:id => t.to_i).exists? | 
					
						
							|  |  |  |         self.tracker_id = t | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2010-11-12 11:34:53 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2017-03-08 21:22:08 +00:00
										 |  |  |     if project && tracker.nil? | 
					
						
							| 
									
										
										
										
											2016-05-30 18:20:13 +00:00
										 |  |  |       # Set a default tracker to accept custom field values | 
					
						
							| 
									
										
										
										
											2015-03-14 09:53:36 +00:00
										 |  |  |       # even if tracker is not specified | 
					
						
							| 
									
										
										
										
											2017-03-08 21:22:08 +00:00
										 |  |  |       allowed_trackers = allowed_target_trackers(user) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if attrs['parent_issue_id'].present? | 
					
						
							|  |  |  |         # If parent_issue_id is present, the first tracker for which this field | 
					
						
							|  |  |  |         # is not disabled is chosen as default | 
					
						
							|  |  |  |         self.tracker = allowed_trackers.detect {|t| t.core_fields.include?('parent_issue_id')} | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       self.tracker ||= allowed_trackers.first | 
					
						
							| 
									
										
										
										
											2015-03-14 09:53:36 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-26 08:30:19 +00:00
										 |  |  |     statuses_allowed = new_statuses_allowed_to(user) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     if (s = attrs.delete('status_id')) && safe_attribute?('status_id') | 
					
						
							| 
									
										
										
										
											2015-07-26 08:30:19 +00:00
										 |  |  |       if statuses_allowed.collect(&:id).include?(s.to_i) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |         self.status_id = s | 
					
						
							| 
									
										
										
										
											2010-02-23 21:26:29 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2015-07-26 08:30:19 +00:00
										 |  |  |     if new_record? && !statuses_allowed.include?(status) | 
					
						
							|  |  |  |       self.status = statuses_allowed.first || default_status | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-03-12 13:09:23 +00:00
										 |  |  |     if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id') | 
					
						
							| 
									
										
										
										
											2016-12-10 12:02:37 +00:00
										 |  |  |       self.assigned_to_id = u | 
					
						
							| 
									
										
										
										
											2016-03-12 13:09:23 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     attrs = delete_unsafe_attributes(attrs, user) | 
					
						
							|  |  |  |     return if attrs.empty? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-12-13 19:56:33 +00:00
										 |  |  |     if attrs['parent_issue_id'].present? | 
					
						
							| 
									
										
										
										
											2012-10-14 14:35:57 +00:00
										 |  |  |       s = attrs['parent_issue_id'].to_s | 
					
						
							| 
									
										
										
										
											2013-01-20 16:41:31 +00:00
										 |  |  |       unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1])) | 
					
						
							| 
									
										
										
										
											2012-10-12 13:40:41 +00:00
										 |  |  |         @invalid_parent_issue_id = attrs.delete('parent_issue_id') | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     if attrs['custom_field_values'].present? | 
					
						
							| 
									
										
										
										
											2013-07-13 09:20:11 +00:00
										 |  |  |       editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s} | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |       attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)} | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if attrs['custom_fields'].present? | 
					
						
							| 
									
										
										
										
											2013-07-13 09:20:11 +00:00
										 |  |  |       editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s} | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |       attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)} | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |     assign_attributes attrs | 
					
						
							| 
									
										
										
										
											2010-01-12 20:17:20 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-05 12:20:07 +00:00
										 |  |  |   def disabled_core_fields | 
					
						
							|  |  |  |     tracker ? tracker.disabled_core_fields : [] | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   # Returns the custom_field_values that can be edited by the given user | 
					
						
							|  |  |  |   def editable_custom_field_values(user=nil) | 
					
						
							| 
									
										
										
										
											2016-08-20 11:38:27 +00:00
										 |  |  |     read_only = read_only_attribute_names(user) | 
					
						
							| 
									
										
										
										
											2013-07-13 09:20:11 +00:00
										 |  |  |     visible_custom_field_values(user).reject do |value| | 
					
						
							| 
									
										
										
										
											2016-08-20 11:38:27 +00:00
										 |  |  |       read_only.include?(value.custom_field_id.to_s) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-05-02 07:32:41 +00:00
										 |  |  |   # Returns the custom fields that can be edited by the given user | 
					
						
							|  |  |  |   def editable_custom_fields(user=nil) | 
					
						
							|  |  |  |     editable_custom_field_values(user).map(&:custom_field).uniq | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   # Returns the names of attributes that are read-only for user or the current user | 
					
						
							|  |  |  |   # For users with multiple roles, the read-only fields are the intersection of | 
					
						
							|  |  |  |   # read-only fields of each role | 
					
						
							|  |  |  |   # The result is an array of strings where sustom fields are represented with their ids | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   # Examples: | 
					
						
							|  |  |  |   #   issue.read_only_attribute_names # => ['due_date', '2'] | 
					
						
							|  |  |  |   #   issue.read_only_attribute_names(user) # => [] | 
					
						
							|  |  |  |   def read_only_attribute_names(user=nil) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:50:38 +00:00
										 |  |  |     workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns the names of required attributes for user or the current user | 
					
						
							|  |  |  |   # For users with multiple roles, the required fields are the intersection of | 
					
						
							|  |  |  |   # required fields of each role | 
					
						
							|  |  |  |   # The result is an array of strings where sustom fields are represented with their ids | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   # Examples: | 
					
						
							|  |  |  |   #   issue.required_attribute_names # => ['due_date', '2'] | 
					
						
							|  |  |  |   #   issue.required_attribute_names(user) # => [] | 
					
						
							|  |  |  |   def required_attribute_names(user=nil) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:50:38 +00:00
										 |  |  |     workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns true if the attribute is required for user | 
					
						
							|  |  |  |   def required_attribute?(name, user=nil) | 
					
						
							|  |  |  |     required_attribute_names(user).include?(name.to_s) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns a hash of the workflow rule by attribute for the given user | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   # Examples: | 
					
						
							|  |  |  |   #   issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'} | 
					
						
							|  |  |  |   def workflow_rule_by_attribute(user=nil) | 
					
						
							|  |  |  |     return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     user_real = user || User.current | 
					
						
							| 
									
										
										
										
											2014-10-26 22:20:10 +00:00
										 |  |  |     roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project) | 
					
						
							| 
									
										
										
										
											2014-12-13 14:31:58 +00:00
										 |  |  |     roles = roles.select(&:consider_workflow?) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     return {} if roles.empty? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     result = {} | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |     workflow_permissions = | 
					
						
							|  |  |  |       WorkflowPermission.where( | 
					
						
							|  |  |  |         :tracker_id => tracker_id, :old_status_id => status_id, | 
					
						
							|  |  |  |         :role_id => roles.map(&:id) | 
					
						
							|  |  |  |       ).to_a | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |     if workflow_permissions.any? | 
					
						
							|  |  |  |       workflow_rules = workflow_permissions.inject({}) do |h, wp| | 
					
						
							| 
									
										
										
										
											2015-03-20 09:17:54 +00:00
										 |  |  |         h[wp.field_name] ||= {} | 
					
						
							|  |  |  |         h[wp.field_name][wp.role_id] = wp.rule | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |         h | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2015-03-20 09:17:54 +00:00
										 |  |  |       fields_with_roles = {} | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |       IssueCustomField.where(:visible => false). | 
					
						
							|  |  |  |         joins(:roles).pluck(:id, "role_id"). | 
					
						
							|  |  |  |           each do |field_id, role_id| | 
					
						
							| 
									
										
										
										
											2015-03-20 09:17:54 +00:00
										 |  |  |         fields_with_roles[field_id] ||= [] | 
					
						
							|  |  |  |         fields_with_roles[field_id] << role_id | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       roles.each do |role| | 
					
						
							|  |  |  |         fields_with_roles.each do |field_id, role_ids| | 
					
						
							|  |  |  |           unless role_ids.include?(role.id) | 
					
						
							|  |  |  |             field_name = field_id.to_s | 
					
						
							|  |  |  |             workflow_rules[field_name] ||= {} | 
					
						
							|  |  |  |             workflow_rules[field_name][role.id] = 'readonly' | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |       workflow_rules.each do |attr, rules| | 
					
						
							|  |  |  |         next if rules.size < roles.size | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-20 09:17:54 +00:00
										 |  |  |         uniq_rules = rules.values.uniq | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |         if uniq_rules.size == 1
 | 
					
						
							|  |  |  |           result[attr] = uniq_rules.first | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |           result[attr] = 'required' | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     @workflow_rule_by_attribute = result if user.nil? | 
					
						
							|  |  |  |     result | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  |   private :workflow_rule_by_attribute | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-11 18:48:34 +00:00
										 |  |  |   def done_ratio | 
					
						
							| 
									
										
										
										
											2010-09-26 18:13:31 +00:00
										 |  |  |     if Issue.use_status_for_done_ratio? && status && status.default_done_ratio | 
					
						
							| 
									
										
										
										
											2009-12-12 11:02:53 +00:00
										 |  |  |       status.default_done_ratio | 
					
						
							| 
									
										
										
										
											2009-12-11 18:48:34 +00:00
										 |  |  |     else | 
					
						
							|  |  |  |       read_attribute(:done_ratio) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def self.use_status_for_done_ratio? | 
					
						
							|  |  |  |     Setting.issue_done_ratio == 'issue_status' | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def self.use_field_for_done_ratio? | 
					
						
							|  |  |  |     Setting.issue_done_ratio == 'issue_field' | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-09-21 06:12:35 +00:00
										 |  |  |   def validate_issue | 
					
						
							| 
									
										
										
										
											2013-06-05 19:56:59 +00:00
										 |  |  |     if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date | 
					
						
							| 
									
										
										
										
											2009-02-21 11:04:50 +00:00
										 |  |  |       errors.add :due_date, :greater_than_start_date | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-06-05 19:56:59 +00:00
										 |  |  |     if start_date && start_date_changed? && soonest_start && start_date < soonest_start | 
					
						
							| 
									
										
										
										
											2013-06-04 17:17:48 +00:00
										 |  |  |       errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start) | 
					
						
							| 
									
										
										
										
											2007-05-05 13:22:27 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-14 14:33:07 +00:00
										 |  |  |     if project && fixed_version | 
					
						
							| 
									
										
										
										
											2009-11-08 13:03:41 +00:00
										 |  |  |       if !assignable_versions.include?(fixed_version) | 
					
						
							|  |  |  |         errors.add :fixed_version_id, :inclusion | 
					
						
							| 
									
										
										
										
											2014-10-25 09:35:17 +00:00
										 |  |  |       elsif reopening? && fixed_version.closed? | 
					
						
							| 
									
										
										
										
											2011-10-06 22:49:26 +00:00
										 |  |  |         errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version) | 
					
						
							| 
									
										
										
										
											2009-11-08 13:03:41 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-11-29 19:46:40 +00:00
										 |  |  |     # Checks that the issue can not be added/moved to a disabled tracker | 
					
						
							|  |  |  |     if project && (tracker_id_changed? || project_id_changed?) | 
					
						
							| 
									
										
										
										
											2016-06-07 18:29:17 +00:00
										 |  |  |       if tracker && !project.trackers.include?(tracker) | 
					
						
							| 
									
										
										
										
											2009-11-29 19:46:40 +00:00
										 |  |  |         errors.add :tracker_id, :inclusion | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-14 14:33:07 +00:00
										 |  |  |     if project && assigned_to_id_changed? && assigned_to_id.present? | 
					
						
							| 
									
										
										
										
											2016-12-10 12:02:37 +00:00
										 |  |  |       unless assignable_users.include?(assigned_to) | 
					
						
							|  |  |  |         errors.add :assigned_to_id, :invalid | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |     # Checks parent issue assignment | 
					
						
							| 
									
										
										
										
											2012-10-12 13:40:41 +00:00
										 |  |  |     if @invalid_parent_issue_id.present? | 
					
						
							|  |  |  |       errors.add :parent_issue_id, :invalid | 
					
						
							|  |  |  |     elsif @parent_issue | 
					
						
							| 
									
										
										
										
											2012-10-10 17:38:17 +00:00
										 |  |  |       if !valid_parent_project?(@parent_issue) | 
					
						
							|  |  |  |         errors.add :parent_issue_id, :invalid | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |       elsif (@parent_issue != parent) && ( | 
					
						
							|  |  |  |           self.would_reschedule?(@parent_issue) || | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |           @parent_issue.self_and_ancestors.any? do |a| | 
					
						
							|  |  |  |             a.relations_from.any? do |r| | 
					
						
							|  |  |  |               r.relation_type == IssueRelation::TYPE_PRECEDES && | 
					
						
							|  |  |  |                  r.issue_to.would_reschedule?(self) | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2013-03-17 15:02:57 +00:00
										 |  |  |         errors.add :parent_issue_id, :invalid | 
					
						
							| 
									
										
										
										
											2016-12-21 09:09:22 +00:00
										 |  |  |       elsif !closed? && @parent_issue.closed? | 
					
						
							|  |  |  |         # cannot attach an open issue to a closed parent | 
					
						
							|  |  |  |         errors.add :base, :open_issue_with_closed_parent | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |       elsif !new_record? | 
					
						
							|  |  |  |         # moving an existing issue | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |         if move_possible?(@parent_issue) | 
					
						
							|  |  |  |           # move accepted | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |         else | 
					
						
							| 
									
										
										
										
											2012-10-10 17:38:17 +00:00
										 |  |  |           errors.add :parent_issue_id, :invalid | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |   # Validates the issue against additional workflow requirements | 
					
						
							|  |  |  |   def validate_required_fields | 
					
						
							|  |  |  |     user = new_record? ? author : current_journal.try(:user) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     required_attribute_names(user).each do |attribute| | 
					
						
							| 
									
										
										
										
											2019-03-27 02:15:24 +00:00
										 |  |  |       if /^\d+$/.match?(attribute) | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |         attribute = attribute.to_i | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:53 +00:00
										 |  |  |         v = custom_field_values.detect {|v| v.custom_field_id == attribute} | 
					
						
							| 
									
										
										
										
											2015-10-14 18:02:12 +00:00
										 |  |  |         if v && Array(v.value).detect(&:present?).nil? | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |           errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       else | 
					
						
							| 
									
										
										
										
											2014-11-04 20:49:46 +00:00
										 |  |  |         if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute) | 
					
						
							| 
									
										
										
										
											2015-10-24 06:22:55 +00:00
										 |  |  |           next if attribute == 'category_id' && project.try(:issue_categories).blank? | 
					
						
							|  |  |  |           next if attribute == 'fixed_version_id' && assignable_versions.blank? | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-07-15 14:12:17 +00:00
										 |  |  |           errors.add attribute, :blank | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-27 08:34:41 +00:00
										 |  |  |   def validate_permissions | 
					
						
							|  |  |  |     if @attributes_set_by && new_record? && copy? | 
					
						
							|  |  |  |       unless allowed_target_trackers(@attributes_set_by).include?(tracker) | 
					
						
							|  |  |  |         errors.add :tracker, :invalid | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-01 13:23:52 +00:00
										 |  |  |   # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values | 
					
						
							|  |  |  |   # so that custom values that are not editable are not validated (eg. a custom field that | 
					
						
							|  |  |  |   # is marked as required should not trigger a validation error if the user is not allowed | 
					
						
							|  |  |  |   # to edit this field). | 
					
						
							|  |  |  |   def validate_custom_field_values | 
					
						
							|  |  |  |     user = new_record? ? author : current_journal.try(:user) | 
					
						
							|  |  |  |     if new_record? || custom_field_values_changed? | 
					
						
							|  |  |  |       editable_custom_field_values(user).each(&:validate_value) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-11 18:48:34 +00:00
										 |  |  |   # Set the done_ratio using the status if that setting is set.  This will keep the done_ratios | 
					
						
							|  |  |  |   # even if the user turns off the setting later | 
					
						
							|  |  |  |   def update_done_ratio_from_issue_status | 
					
						
							| 
									
										
										
										
											2010-09-26 18:13:31 +00:00
										 |  |  |     if Issue.use_status_for_done_ratio? && status && status.default_done_ratio | 
					
						
							| 
									
										
										
										
											2009-12-12 10:33:12 +00:00
										 |  |  |       self.done_ratio = status.default_done_ratio | 
					
						
							| 
									
										
										
										
											2009-12-11 18:48:34 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-03-12 17:59:02 +00:00
										 |  |  |   def init_journal(user, notes = "") | 
					
						
							|  |  |  |     @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-11-22 11:59:33 +00:00
										 |  |  |   # Returns the current journal or nil if it's not initialized | 
					
						
							| 
									
										
										
										
											2014-11-02 15:38:11 +00:00
										 |  |  |   def current_journal | 
					
						
							|  |  |  |     @current_journal | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-03 08:10:20 +00:00
										 |  |  |   # Clears the current journal | 
					
						
							|  |  |  |   def clear_journal | 
					
						
							|  |  |  |     @current_journal = nil | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-12-05 08:03:32 +00:00
										 |  |  |   # Returns the names of attributes that are journalized when updating the issue | 
					
						
							|  |  |  |   def journalized_attribute_names | 
					
						
							| 
									
										
										
										
											2015-04-14 17:16:52 +00:00
										 |  |  |     names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on) | 
					
						
							|  |  |  |     if tracker | 
					
						
							|  |  |  |       names -= tracker.disabled_core_fields | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     names | 
					
						
							| 
									
										
										
										
											2014-12-05 08:03:32 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-02-04 17:36:15 +00:00
										 |  |  |   # Returns the id of the last journal or nil | 
					
						
							|  |  |  |   def last_journal_id | 
					
						
							|  |  |  |     if new_record? | 
					
						
							|  |  |  |       nil | 
					
						
							|  |  |  |     else | 
					
						
							| 
									
										
										
										
											2012-08-08 18:47:52 +00:00
										 |  |  |       journals.maximum(:id) | 
					
						
							| 
									
										
										
										
											2012-02-04 17:36:15 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-08-08 21:28:07 +00:00
										 |  |  |   # Returns a scope for journals that have an id greater than journal_id | 
					
						
							|  |  |  |   def journals_after(journal_id) | 
					
						
							|  |  |  |     scope = journals.reorder("#{Journal.table_name}.id ASC") | 
					
						
							|  |  |  |     if journal_id.present? | 
					
						
							|  |  |  |       scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     scope | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-14 09:34:24 +00:00
										 |  |  |   # Returns the journals that are visible to user with their index | 
					
						
							|  |  |  |   # Used to display the issue history | 
					
						
							|  |  |  |   def visible_journals_with_index(user=User.current) | 
					
						
							|  |  |  |     result = journals. | 
					
						
							|  |  |  |       preload(:details). | 
					
						
							|  |  |  |       preload(:user => :email_address). | 
					
						
							|  |  |  |       reorder(:created_on, :id).to_a | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:45 +00:00
										 |  |  |     result.each_with_index {|j, i| j.indice = i + 1} | 
					
						
							| 
									
										
										
										
											2017-01-14 09:50:28 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     unless user.allowed_to?(:view_private_notes, project) | 
					
						
							|  |  |  |       result.select! do |journal| | 
					
						
							|  |  |  |         !journal.private_notes? || journal.user == user | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2017-01-14 09:34:24 +00:00
										 |  |  |     Journal.preload_journals_details_custom_fields(result) | 
					
						
							|  |  |  |     result.select! {|journal| journal.notes? || journal.visible_details.any?} | 
					
						
							|  |  |  |     result | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-02-17 09:30:17 +00:00
										 |  |  |   # Returns the initial status of the issue | 
					
						
							|  |  |  |   # Returns nil for a new issue | 
					
						
							|  |  |  |   def status_was | 
					
						
							| 
									
										
										
										
											2014-11-08 11:17:45 +00:00
										 |  |  |     if status_id_changed? | 
					
						
							|  |  |  |       if status_id_was.to_i > 0
 | 
					
						
							|  |  |  |         @status_was ||= IssueStatus.find_by_id(status_id_was) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       @status_was ||= status | 
					
						
							| 
									
										
										
										
											2013-02-17 09:30:17 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-08-25 17:45:51 +00:00
										 |  |  |   # Return true if the issue is closed, otherwise false | 
					
						
							|  |  |  |   def closed? | 
					
						
							| 
									
										
										
										
											2014-10-24 21:40:26 +00:00
										 |  |  |     status.present? && status.is_closed? | 
					
						
							| 
									
										
										
										
											2007-08-25 17:45:51 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-10-25 09:35:17 +00:00
										 |  |  |   # Returns true if the issue was closed when loaded | 
					
						
							|  |  |  |   def was_closed? | 
					
						
							|  |  |  |     status_was.present? && status_was.is_closed? | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-11-08 13:03:41 +00:00
										 |  |  |   # Return true if the issue is being reopened | 
					
						
							| 
									
										
										
										
											2014-10-25 09:35:17 +00:00
										 |  |  |   def reopening? | 
					
						
							|  |  |  |     if new_record? | 
					
						
							|  |  |  |       false | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       status_id_changed? && !closed? && was_closed? | 
					
						
							| 
									
										
										
										
											2009-11-08 13:03:41 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2014-10-25 09:35:17 +00:00
										 |  |  |   alias :reopened? :reopening? | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   # Return true if the issue is being closed | 
					
						
							|  |  |  |   def closing? | 
					
						
							| 
									
										
										
										
											2014-10-25 09:35:17 +00:00
										 |  |  |     if new_record? | 
					
						
							|  |  |  |       closed? | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       status_id_changed? && closed? && !was_closed? | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-12-16 21:13:35 +00:00
										 |  |  |   # Returns true if the issue is overdue | 
					
						
							|  |  |  |   def overdue? | 
					
						
							| 
									
										
										
										
											2016-05-07 10:42:22 +00:00
										 |  |  |     due_date.present? && (due_date < User.current.today) && !closed? | 
					
						
							| 
									
										
										
										
											2008-12-16 21:13:35 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2010-09-10 03:09:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   # Is the amount of work done less than it should for the due date | 
					
						
							|  |  |  |   def behind_schedule? | 
					
						
							|  |  |  |     return false if start_date.nil? || due_date.nil? | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-06 06:27:16 +00:00
										 |  |  |     done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor | 
					
						
							| 
									
										
										
										
											2016-05-07 10:42:22 +00:00
										 |  |  |     return done_date <= User.current.today | 
					
						
							| 
									
										
										
										
											2010-09-10 03:09:02 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2010-09-20 02:55:26 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   # Does this issue have children? | 
					
						
							|  |  |  |   def children? | 
					
						
							|  |  |  |     !leaf? | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-08-16 17:47:41 +00:00
										 |  |  |   # Users the issue can be assigned to | 
					
						
							|  |  |  |   def assignable_users | 
					
						
							| 
									
										
										
										
											2020-05-14 14:33:07 +00:00
										 |  |  |     return [] if project.nil? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-06-28 20:31:08 +00:00
										 |  |  |     users = project.assignable_users(tracker).to_a | 
					
						
							| 
									
										
										
										
											2015-12-12 12:37:56 +00:00
										 |  |  |     users << author if author && author.active? | 
					
						
							| 
									
										
										
										
											2016-12-10 12:02:37 +00:00
										 |  |  |     if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was) | 
					
						
							|  |  |  |       users << assignee | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2010-10-07 17:46:37 +00:00
										 |  |  |     users.uniq.sort | 
					
						
							| 
									
										
										
										
											2007-08-16 17:47:41 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-11-08 13:03:41 +00:00
										 |  |  |   # Versions that the issue can be assigned to | 
					
						
							|  |  |  |   def assignable_versions | 
					
						
							| 
									
										
										
										
											2012-07-27 19:36:53 +00:00
										 |  |  |     return @assignable_versions if @assignable_versions | 
					
						
							| 
									
										
										
										
											2020-05-14 14:33:07 +00:00
										 |  |  |     return [] if project.nil? | 
					
						
							| 
									
										
										
										
											2012-07-27 19:36:53 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |     versions = project.shared_versions.open.to_a | 
					
						
							| 
									
										
										
										
											2012-07-27 19:36:53 +00:00
										 |  |  |     if fixed_version | 
					
						
							|  |  |  |       if fixed_version_id_changed? | 
					
						
							|  |  |  |         # nothing to do | 
					
						
							|  |  |  |       elsif project_id_changed? | 
					
						
							|  |  |  |         if project.shared_versions.include?(fixed_version) | 
					
						
							|  |  |  |           versions << fixed_version | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         versions << fixed_version | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     @assignable_versions = versions.uniq.sort | 
					
						
							| 
									
										
										
										
											2009-11-08 13:03:41 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-07-04 12:07:03 +00:00
										 |  |  |   # Returns true if this issue is blocked by another issue that is still open | 
					
						
							|  |  |  |   def blocked? | 
					
						
							|  |  |  |     !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-10 03:50:47 +00:00
										 |  |  |   # Returns true if this issue can be closed and if not, returns false and populates the reason | 
					
						
							|  |  |  |   def closable? | 
					
						
							|  |  |  |     if descendants.open.any? | 
					
						
							|  |  |  |       @transition_warning = l(:notice_issue_not_closable_by_open_tasks) | 
					
						
							|  |  |  |       return false | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     if blocked? | 
					
						
							|  |  |  |       @transition_warning = l(:notice_issue_not_closable_by_blocking_issue) | 
					
						
							|  |  |  |       return false | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     return true | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns true if this issue can be reopen and if not, returns false and populates the reason | 
					
						
							|  |  |  |   def reopenable? | 
					
						
							|  |  |  |     if ancestors.open(false).any? | 
					
						
							|  |  |  |       @transition_warning = l(:notice_issue_not_reopenable_by_closed_parent_issue) | 
					
						
							|  |  |  |       return false | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     return true | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-11-02 19:45:14 +00:00
										 |  |  |   # Returns the default status of the issue based on its tracker | 
					
						
							|  |  |  |   # Returns nil if tracker is nil | 
					
						
							|  |  |  |   def default_status | 
					
						
							|  |  |  |     tracker.try(:default_status) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-04-09 09:39:27 +00:00
										 |  |  |   # Returns an array of statuses that user is able to apply | 
					
						
							| 
									
										
										
										
											2012-02-09 19:51:38 +00:00
										 |  |  |   def new_statuses_allowed_to(user=User.current, include_default=false) | 
					
						
							| 
									
										
										
										
											2016-09-03 07:21:32 +00:00
										 |  |  |     initial_status = nil | 
					
						
							|  |  |  |     if new_record? | 
					
						
							|  |  |  |       # nop | 
					
						
							|  |  |  |     elsif tracker_id_changed? | 
					
						
							|  |  |  |       if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any? | 
					
						
							|  |  |  |         initial_status = default_status | 
					
						
							|  |  |  |       elsif tracker.issue_status_ids.include?(status_id_was) | 
					
						
							|  |  |  |         initial_status = IssueStatus.find_by_id(status_id_was) | 
					
						
							| 
									
										
										
										
											2014-10-26 22:33:33 +00:00
										 |  |  |       else | 
					
						
							| 
									
										
										
										
											2016-09-03 07:21:32 +00:00
										 |  |  |         initial_status = default_status | 
					
						
							| 
									
										
										
										
											2012-04-14 05:45:16 +00:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-09-03 07:21:32 +00:00
										 |  |  |     else | 
					
						
							|  |  |  |       initial_status = status_was | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2013-07-28 14:58:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-03 07:21:32 +00:00
										 |  |  |     initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id | 
					
						
							|  |  |  |     assignee_transitions_allowed = initial_assigned_to_id.present? && | 
					
						
							|  |  |  |       (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     statuses = [] | 
					
						
							|  |  |  |     statuses += IssueStatus.new_statuses_allowed( | 
					
						
							|  |  |  |       initial_status, | 
					
						
							|  |  |  |       user.admin ? Role.all.to_a : user.roles_for_project(project), | 
					
						
							|  |  |  |       tracker, | 
					
						
							|  |  |  |       author == user, | 
					
						
							|  |  |  |       assignee_transitions_allowed | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     statuses << initial_status unless statuses.empty? | 
					
						
							|  |  |  |     statuses << default_status if include_default || (new_record? && statuses.empty?) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     statuses = statuses.compact.uniq.sort | 
					
						
							| 
									
										
										
										
											2020-03-10 03:50:47 +00:00
										 |  |  |     unless closable? | 
					
						
							| 
									
										
										
										
											2016-12-21 09:09:22 +00:00
										 |  |  |       # cannot close a blocked issue or a parent with open subtasks | 
					
						
							| 
									
										
										
										
											2016-09-03 07:21:32 +00:00
										 |  |  |       statuses.reject!(&:is_closed?) | 
					
						
							| 
									
										
										
										
											2012-04-14 05:45:16 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2020-03-10 03:50:47 +00:00
										 |  |  |     unless reopenable? | 
					
						
							| 
									
										
										
										
											2016-12-21 09:09:22 +00:00
										 |  |  |       # cannot reopen a subtask of a closed parent | 
					
						
							|  |  |  |       statuses.select!(&:is_closed?) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-09-03 07:21:32 +00:00
										 |  |  |     statuses | 
					
						
							| 
									
										
										
										
											2008-01-06 17:06:14 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-09-20 11:18:53 +00:00
										 |  |  |   # Returns the original tracker | 
					
						
							|  |  |  |   def tracker_was | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |     Tracker.find_by_id(tracker_id_in_database) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns the previous assignee whenever we're before the save | 
					
						
							|  |  |  |   # or in after_* callbacks | 
					
						
							|  |  |  |   def previous_assignee | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |     previous_assigned_to_id = | 
					
						
							|  |  |  |       if assigned_to_id_change_to_be_saved.nil? | 
					
						
							|  |  |  |         assigned_to_id_before_last_save | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         assigned_to_id_in_database | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     if previous_assigned_to_id | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |       Principal.find_by_id(previous_assigned_to_id) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2015-09-20 11:18:53 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-10-03 21:36:19 +00:00
										 |  |  |   # Returns the users that should be notified | 
					
						
							|  |  |  |   def notified_users | 
					
						
							| 
									
										
										
										
											2010-09-28 18:22:10 +00:00
										 |  |  |     # Author and assignee are always notified unless they have been | 
					
						
							|  |  |  |     # locked or don't want to be notified | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |     notified = [author, assigned_to, previous_assignee].compact.uniq | 
					
						
							|  |  |  |     notified = notified.map {|n| n.is_a?(Group) ? n.users : n}.flatten | 
					
						
							|  |  |  |     notified.uniq! | 
					
						
							| 
									
										
										
										
											2012-01-23 17:55:29 +00:00
										 |  |  |     notified = notified.select {|u| u.active? && u.notify_about?(self)} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     notified += project.notified_users | 
					
						
							| 
									
										
										
										
											2020-01-21 04:23:26 +00:00
										 |  |  |     notified += project.users.preload(:preference).select(&:notify_about_high_priority_issues?) if priority.high? | 
					
						
							| 
									
										
										
										
											2009-12-03 21:28:14 +00:00
										 |  |  |     notified.uniq! | 
					
						
							|  |  |  |     # Remove users that can not view the issue | 
					
						
							|  |  |  |     notified.reject! {|user| !visible?(user)} | 
					
						
							| 
									
										
										
										
											2012-10-03 21:36:19 +00:00
										 |  |  |     notified | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns the email addresses that should be notified | 
					
						
							|  |  |  |   def recipients | 
					
						
							|  |  |  |     notified_users.collect(&:mail) | 
					
						
							| 
									
										
										
										
											2009-12-03 21:28:14 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-14 08:20:32 +00:00
										 |  |  |   def notify? | 
					
						
							|  |  |  |     @notify != false | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def notify=(arg) | 
					
						
							|  |  |  |     @notify = arg | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-12-04 16:43:32 +00:00
										 |  |  |   # Returns the number of hours spent on this issue | 
					
						
							|  |  |  |   def spent_hours | 
					
						
							| 
									
										
										
										
											2017-04-05 16:09:58 +00:00
										 |  |  |     @spent_hours ||= time_entries.sum(:hours) || 0.0
 | 
					
						
							| 
									
										
										
										
											2011-12-04 16:43:32 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   # Returns the total number of hours spent on this issue and its descendants | 
					
						
							| 
									
										
										
										
											2011-12-04 16:43:32 +00:00
										 |  |  |   def total_spent_hours | 
					
						
							| 
									
										
										
										
											2019-09-30 11:42:48 +00:00
										 |  |  |     @total_spent_hours ||= | 
					
						
							|  |  |  |       if leaf? | 
					
						
							|  |  |  |         spent_hours | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
 | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2007-03-23 12:22:31 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-05-25 11:37:12 +00:00
										 |  |  |   def total_estimated_hours | 
					
						
							|  |  |  |     if leaf? | 
					
						
							|  |  |  |       estimated_hours | 
					
						
							|  |  |  |     else | 
					
						
							| 
									
										
										
										
											2019-08-07 04:44:19 +00:00
										 |  |  |       @total_estimated_hours ||= self_and_descendants.visible.sum(:estimated_hours) | 
					
						
							| 
									
										
										
										
											2015-05-25 11:37:12 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-05-05 13:22:27 +00:00
										 |  |  |   def relations | 
					
						
							| 
									
										
										
										
											2012-12-10 21:59:01 +00:00
										 |  |  |     @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort) | 
					
						
							| 
									
										
										
										
											2011-07-24 15:34:41 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-08-21 08:46:10 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-05 07:42:52 +00:00
										 |  |  |   def last_updated_by | 
					
						
							|  |  |  |     if @last_updated_by | 
					
						
							|  |  |  |       @last_updated_by.presence | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       journals.reorder(:id => :desc).first.try(:user) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-05 07:58:07 +00:00
										 |  |  |   def last_notes | 
					
						
							|  |  |  |     if @last_notes | 
					
						
							|  |  |  |       @last_notes | 
					
						
							|  |  |  |     else | 
					
						
							| 
									
										
										
										
											2017-03-05 08:08:21 +00:00
										 |  |  |       journals.where.not(notes: '').reorder(:id => :desc).first.try(:notes) | 
					
						
							| 
									
										
										
										
											2017-03-05 07:58:07 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-07-24 15:34:41 +00:00
										 |  |  |   # Preloads relations for a collection of issues | 
					
						
							|  |  |  |   def self.load_relations(issues) | 
					
						
							|  |  |  |     if issues.any? | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |       relations = | 
					
						
							|  |  |  |         IssueRelation.where( | 
					
						
							|  |  |  |           "issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id) | 
					
						
							|  |  |  |         ).all | 
					
						
							| 
									
										
										
										
											2011-07-24 15:34:41 +00:00
										 |  |  |       issues.each do |issue| | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |         issue.instance_variable_set( | 
					
						
							|  |  |  |           "@relations", | 
					
						
							|  |  |  |           relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id} | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2011-07-24 15:34:41 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2007-05-05 13:22:27 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-08-21 08:46:10 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-05 12:53:57 +00:00
										 |  |  |   # Preloads visible spent time for a collection of issues | 
					
						
							| 
									
										
										
										
											2011-12-04 16:43:32 +00:00
										 |  |  |   def self.load_visible_spent_hours(issues, user=User.current) | 
					
						
							|  |  |  |     if issues.any? | 
					
						
							| 
									
										
										
										
											2015-07-05 12:53:32 +00:00
										 |  |  |       hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours) | 
					
						
							| 
									
										
										
										
											2011-12-04 16:43:32 +00:00
										 |  |  |       issues.each do |issue| | 
					
						
							| 
									
										
										
										
											2017-04-05 16:09:58 +00:00
										 |  |  |         issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0.0) | 
					
						
							| 
									
										
										
										
											2015-07-05 12:38:39 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-05 12:53:57 +00:00
										 |  |  |   # Preloads visible total spent time for a collection of issues | 
					
						
							| 
									
										
										
										
											2015-07-05 12:38:39 +00:00
										 |  |  |   def self.load_visible_total_spent_hours(issues, user=User.current) | 
					
						
							|  |  |  |     if issues.any? | 
					
						
							| 
									
										
										
										
											2015-07-05 12:53:32 +00:00
										 |  |  |       hours_by_issue_id = TimeEntry.visible(user).joins(:issue). | 
					
						
							|  |  |  |         joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" + | 
					
						
							|  |  |  |           " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt"). | 
					
						
							|  |  |  |         where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours) | 
					
						
							| 
									
										
										
										
											2015-07-05 12:38:39 +00:00
										 |  |  |       issues.each do |issue| | 
					
						
							| 
									
										
										
										
											2017-04-05 16:09:58 +00:00
										 |  |  |         issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0.0) | 
					
						
							| 
									
										
										
										
											2011-12-04 16:43:32 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-09-29 12:57:38 +00:00
										 |  |  |   # Preloads visible relations for a collection of issues | 
					
						
							|  |  |  |   def self.load_visible_relations(issues, user=User.current) | 
					
						
							|  |  |  |     if issues.any? | 
					
						
							|  |  |  |       issue_ids = issues.map(&:id) | 
					
						
							|  |  |  |       # Relations with issue_from in given issues and visible issue_to | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |       relations_from = IssueRelation.joins(:issue_to => :project). | 
					
						
							|  |  |  |                          where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a | 
					
						
							| 
									
										
										
										
											2012-09-29 12:57:38 +00:00
										 |  |  |       # Relations with issue_to in given issues and visible issue_from | 
					
						
							| 
									
										
										
										
											2014-10-22 17:37:16 +00:00
										 |  |  |       relations_to = IssueRelation.joins(:issue_from => :project). | 
					
						
							|  |  |  |                          where(visible_condition(user)). | 
					
						
							|  |  |  |                          where(:issue_to_id => issue_ids).to_a | 
					
						
							| 
									
										
										
										
											2012-09-29 12:57:38 +00:00
										 |  |  |       issues.each do |issue| | 
					
						
							|  |  |  |         relations = | 
					
						
							|  |  |  |           relations_from.select {|relation| relation.issue_from_id == issue.id} + | 
					
						
							|  |  |  |           relations_to.select {|relation| relation.issue_to_id == issue.id} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-12-10 21:59:01 +00:00
										 |  |  |         issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort) | 
					
						
							| 
									
										
										
										
											2012-09-29 12:57:38 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-12-31 15:04:05 +00:00
										 |  |  |   # Returns a scope of the given issues and their descendants | 
					
						
							|  |  |  |   def self.self_and_descendants(issues) | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:21 +00:00
										 |  |  |     Issue.joins( | 
					
						
							| 
									
										
										
										
											2020-09-26 15:02:02 +00:00
										 |  |  |       "JOIN #{Issue.table_name} ancestors" + | 
					
						
							|  |  |  |       " ON ancestors.root_id = #{Issue.table_name}.root_id" + | 
					
						
							|  |  |  |       " AND ancestors.lft <= #{Issue.table_name}.lft AND ancestors.rgt >= #{Issue.table_name}.rgt" | 
					
						
							|  |  |  |     ). | 
					
						
							| 
									
										
										
										
											2016-12-31 15:04:05 +00:00
										 |  |  |       where(:ancestors => {:id => issues.map(&:id)}) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-05 07:42:52 +00:00
										 |  |  |   # Preloads users who updated last a collection of issues | 
					
						
							|  |  |  |   def self.load_visible_last_updated_by(issues, user=User.current) | 
					
						
							| 
									
										
										
										
											2017-03-13 21:44:53 +00:00
										 |  |  |     if issues.any? | 
					
						
							| 
									
										
										
										
											2017-03-05 07:42:52 +00:00
										 |  |  |       issue_ids = issues.map(&:id) | 
					
						
							| 
									
										
										
										
											2017-03-05 11:05:50 +00:00
										 |  |  |       journal_ids = Journal.joins(issue: :project). | 
					
						
							| 
									
										
										
										
											2017-03-05 07:42:52 +00:00
										 |  |  |         where(:journalized_type => 'Issue', :journalized_id => issue_ids). | 
					
						
							| 
									
										
										
										
											2017-03-05 11:05:50 +00:00
										 |  |  |         where(Journal.visible_notes_condition(user, :skip_pre_condition => true)). | 
					
						
							|  |  |  |         group(:journalized_id). | 
					
						
							|  |  |  |         maximum(:id). | 
					
						
							|  |  |  |         values | 
					
						
							| 
									
										
										
										
											2017-03-05 15:16:24 +00:00
										 |  |  |       journals = Journal.where(:id => journal_ids).preload(:user).to_a | 
					
						
							| 
									
										
										
										
											2017-03-05 07:42:52 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       issues.each do |issue| | 
					
						
							|  |  |  |         journal = journals.detect {|j| j.journalized_id == issue.id} | 
					
						
							|  |  |  |         issue.instance_variable_set("@last_updated_by", journal.try(:user) || '') | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-05 07:58:07 +00:00
										 |  |  |   # Preloads visible last notes for a collection of issues | 
					
						
							|  |  |  |   def self.load_visible_last_notes(issues, user=User.current) | 
					
						
							|  |  |  |     if issues.any? | 
					
						
							|  |  |  |       issue_ids = issues.map(&:id) | 
					
						
							| 
									
										
										
										
											2017-03-05 11:05:50 +00:00
										 |  |  |       journal_ids = Journal.joins(issue: :project). | 
					
						
							| 
									
										
										
										
											2017-03-05 08:05:29 +00:00
										 |  |  |         where(:journalized_type => 'Issue', :journalized_id => issue_ids). | 
					
						
							| 
									
										
										
										
											2017-03-05 11:05:50 +00:00
										 |  |  |         where(Journal.visible_notes_condition(user, :skip_pre_condition => true)). | 
					
						
							|  |  |  |         where.not(notes: ''). | 
					
						
							|  |  |  |         group(:journalized_id). | 
					
						
							|  |  |  |         maximum(:id). | 
					
						
							|  |  |  |         values | 
					
						
							|  |  |  |       journals = Journal.where(:id => journal_ids).to_a | 
					
						
							| 
									
										
										
										
											2017-03-05 07:58:07 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       issues.each do |issue| | 
					
						
							| 
									
										
										
										
											2017-03-05 08:05:29 +00:00
										 |  |  |         journal = journals.detect {|j| j.journalized_id == issue.id} | 
					
						
							|  |  |  |         issue.instance_variable_set("@last_notes", journal.try(:notes) || '') | 
					
						
							| 
									
										
										
										
											2017-03-05 07:58:07 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2011-07-04 17:03:04 +00:00
										 |  |  |   # Finds an issue relation given its id. | 
					
						
							|  |  |  |   def find_relation(relation_id) | 
					
						
							| 
									
										
										
										
											2013-06-12 19:13:25 +00:00
										 |  |  |     IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id) | 
					
						
							| 
									
										
										
										
											2011-07-04 17:03:04 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |   # Returns true if this issue blocks the other issue, otherwise returns false | 
					
						
							|  |  |  |   def blocks?(other) | 
					
						
							|  |  |  |     all = [self] | 
					
						
							|  |  |  |     last = [self] | 
					
						
							|  |  |  |     while last.any? | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |       current = | 
					
						
							|  |  |  |         last.map do |i| | 
					
						
							|  |  |  |           i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to) | 
					
						
							|  |  |  |         end.flatten.uniq | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |       current -= last | 
					
						
							|  |  |  |       current -= all | 
					
						
							|  |  |  |       return true if current.include?(other) | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |       last = current | 
					
						
							|  |  |  |       all += last | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     false | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns true if the other issue might be rescheduled if the start/due dates of this issue change | 
					
						
							|  |  |  |   def would_reschedule?(other) | 
					
						
							|  |  |  |     all = [self] | 
					
						
							|  |  |  |     last = [self] | 
					
						
							|  |  |  |     while last.any? | 
					
						
							| 
									
										
										
										
											2020-11-07 12:30:38 +00:00
										 |  |  |       current = last.map do |i| | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |         i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) + | 
					
						
							|  |  |  |         i.leaves.to_a + | 
					
						
							|  |  |  |         i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)} | 
					
						
							| 
									
										
										
										
											2020-11-07 12:30:38 +00:00
										 |  |  |       end.flatten.uniq | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |       current -= last | 
					
						
							|  |  |  |       current -= all | 
					
						
							|  |  |  |       return true if current.include?(other) | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-10 15:12:28 +00:00
										 |  |  |       last = current | 
					
						
							|  |  |  |       all += last | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     false | 
					
						
							| 
									
										
										
										
											2007-05-05 13:22:27 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-06-03 18:30:29 +00:00
										 |  |  |   # Returns an array of issues that duplicate this one | 
					
						
							| 
									
										
										
										
											2007-08-25 17:45:51 +00:00
										 |  |  |   def duplicates | 
					
						
							| 
									
										
										
										
											2008-06-03 18:30:29 +00:00
										 |  |  |     relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} | 
					
						
							| 
									
										
										
										
											2007-08-25 17:45:51 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-05-25 13:26:21 +00:00
										 |  |  |   # Returns the due date or the target due date if any | 
					
						
							|  |  |  |   # Used on gantt chart | 
					
						
							|  |  |  |   def due_before | 
					
						
							|  |  |  |     due_date || (fixed_version ? fixed_version.effective_date : nil) | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-03-21 00:39:53 +00:00
										 |  |  |   # Returns the time scheduled for this issue. | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  |   # | 
					
						
							| 
									
										
										
										
											2009-03-21 00:39:53 +00:00
										 |  |  |   # Example: | 
					
						
							|  |  |  |   #   Start Date: 2/26/09, End Date: 3/04/09 | 
					
						
							|  |  |  |   #   duration => 6 | 
					
						
							| 
									
										
										
										
											2007-05-05 13:22:27 +00:00
										 |  |  |   def duration | 
					
						
							|  |  |  |     (start_date && due_date) ? due_date - start_date : 0
 | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-10-29 10:06:30 +00:00
										 |  |  |   # Returns the duration in working days | 
					
						
							|  |  |  |   def working_duration | 
					
						
							|  |  |  |     (start_date && due_date) ? working_days(start_date, due_date) : 0
 | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-11-24 13:43:52 +00:00
										 |  |  |   def soonest_start(reload=false) | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |     if @soonest_start.nil? || reload | 
					
						
							| 
									
										
										
										
											2016-07-16 10:48:11 +00:00
										 |  |  |       relations_to.reload if reload | 
					
						
							|  |  |  |       dates = relations_to.collect{|relation| relation.successor_soonest_start} | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |       p = @parent_issue || parent | 
					
						
							|  |  |  |       if p && Setting.parent_issue_dates == 'derived' | 
					
						
							|  |  |  |         dates << p.soonest_start | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       @soonest_start = dates.compact.max | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     @soonest_start | 
					
						
							| 
									
										
										
										
											2010-03-13 15:29:34 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-10-29 10:06:30 +00:00
										 |  |  |   # Sets start_date on the given date or the next working day | 
					
						
							|  |  |  |   # and changes due_date to keep the same working duration. | 
					
						
							|  |  |  |   def reschedule_on(date) | 
					
						
							|  |  |  |     wd = working_duration | 
					
						
							|  |  |  |     date = next_working_date(date) | 
					
						
							|  |  |  |     self.start_date = date | 
					
						
							|  |  |  |     self.due_date = add_working_days(date, wd) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Reschedules the issue on the given date or the next working day and saves the record. | 
					
						
							|  |  |  |   # If the issue is a parent task, this is done by rescheduling its subtasks. | 
					
						
							| 
									
										
										
										
											2018-06-10 05:57:12 +00:00
										 |  |  |   def reschedule_on!(date, journal=nil) | 
					
						
							| 
									
										
										
										
											2010-03-13 15:29:34 +00:00
										 |  |  |     return if date.nil? | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |     if leaf? || !dates_derived? | 
					
						
							| 
									
										
										
										
											2012-11-24 13:43:52 +00:00
										 |  |  |       if start_date.nil? || start_date != date | 
					
						
							|  |  |  |         if start_date && start_date > date | 
					
						
							|  |  |  |           # Issue can not be moved earlier than its soonest start date | 
					
						
							|  |  |  |           date = [soonest_start(true), date].compact.max | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2018-06-10 05:57:12 +00:00
										 |  |  |         if journal | 
					
						
							|  |  |  |           init_journal(journal.user) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2012-10-29 10:06:30 +00:00
										 |  |  |         reschedule_on(date) | 
					
						
							| 
									
										
										
										
											2012-02-12 19:41:42 +00:00
										 |  |  |         begin | 
					
						
							|  |  |  |           save | 
					
						
							|  |  |  |         rescue ActiveRecord::StaleObjectError | 
					
						
							|  |  |  |           reload | 
					
						
							| 
									
										
										
										
											2012-10-29 10:06:30 +00:00
										 |  |  |           reschedule_on(date) | 
					
						
							| 
									
										
										
										
											2012-02-12 19:41:42 +00:00
										 |  |  |           save | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2010-03-13 15:29:34 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       leaves.each do |leaf| | 
					
						
							| 
									
										
										
										
											2012-11-24 15:09:51 +00:00
										 |  |  |         if leaf.start_date | 
					
						
							|  |  |  |           # Only move subtask if it starts at the same date as the parent | 
					
						
							|  |  |  |           # or if it starts before the given date | 
					
						
							| 
									
										
										
										
											2014-04-08 11:22:33 +00:00
										 |  |  |           if start_date == leaf.start_date || date > leaf.start_date | 
					
						
							| 
									
										
										
										
											2012-11-24 15:09:51 +00:00
										 |  |  |             leaf.reschedule_on!(date) | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |           leaf.reschedule_on!(date) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2010-03-13 15:29:34 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2007-05-05 13:22:27 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |   def dates_derived? | 
					
						
							|  |  |  |     !leaf? && Setting.parent_issue_dates == 'derived' | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def priority_derived? | 
					
						
							|  |  |  |     !leaf? && Setting.parent_issue_priority == 'derived' | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-05-25 10:03:42 +00:00
										 |  |  |   def done_ratio_derived? | 
					
						
							|  |  |  |     !leaf? && Setting.parent_issue_done_ratio == 'derived' | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   def <=>(issue) | 
					
						
							|  |  |  |     if issue.nil? | 
					
						
							|  |  |  |       -1
 | 
					
						
							|  |  |  |     elsif root_id != issue.root_id | 
					
						
							|  |  |  |       (root_id || 0) <=> (issue.root_id || 0) | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       (lft || 0) <=> (issue.lft || 0) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-04-01 22:42:10 +00:00
										 |  |  |   def to_s | 
					
						
							|  |  |  |     "#{tracker} ##{id}: #{subject}" | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-04-25 11:28:48 +00:00
										 |  |  |   # Returns a string of css classes that apply to the issue | 
					
						
							| 
									
										
										
										
											2013-07-11 19:35:40 +00:00
										 |  |  |   def css_classes(user=User.current) | 
					
						
							| 
									
										
										
										
											2019-03-17 00:29:58 +00:00
										 |  |  |     s = +"issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}" | 
					
						
							| 
									
										
										
										
											2009-04-25 11:28:48 +00:00
										 |  |  |     s << ' closed' if closed? | 
					
						
							|  |  |  |     s << ' overdue' if overdue? | 
					
						
							| 
									
										
										
										
											2011-03-25 18:31:32 +00:00
										 |  |  |     s << ' child' if child? | 
					
						
							|  |  |  |     s << ' parent' unless leaf? | 
					
						
							| 
									
										
										
										
											2011-04-15 13:23:13 +00:00
										 |  |  |     s << ' private' if is_private? | 
					
						
							| 
									
										
										
										
											2020-07-09 00:45:19 +00:00
										 |  |  |     s << ' behind-schedule' if behind_schedule? | 
					
						
							| 
									
										
										
										
											2013-07-11 19:35:40 +00:00
										 |  |  |     if user.logged? | 
					
						
							|  |  |  |       s << ' created-by-me' if author_id == user.id | 
					
						
							|  |  |  |       s << ' assigned-to-me' if assigned_to_id == user.id | 
					
						
							| 
									
										
										
										
											2014-02-06 12:16:27 +00:00
										 |  |  |       s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id} | 
					
						
							| 
									
										
										
										
											2013-07-11 19:35:40 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2009-04-25 11:28:48 +00:00
										 |  |  |     s | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2009-12-06 10:28:20 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-08 20:47:52 +00:00
										 |  |  |   # Unassigns issues from +version+ if it's no longer shared with issue's project | 
					
						
							|  |  |  |   def self.update_versions_from_sharing_change(version) | 
					
						
							|  |  |  |     # Update issues assigned to the version | 
					
						
							|  |  |  |     update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-08 20:47:52 +00:00
										 |  |  |   # Unassigns issues from versions that are no longer shared | 
					
						
							|  |  |  |   # after +project+ was moved | 
					
						
							|  |  |  |   def self.update_versions_from_hierarchy_change(project) | 
					
						
							| 
									
										
										
										
											2018-07-28 15:24:24 +00:00
										 |  |  |     moved_project_ids = project.self_and_descendants.reload.pluck(:id) | 
					
						
							| 
									
										
										
										
											2009-12-08 20:47:52 +00:00
										 |  |  |     # Update issues of the moved projects and issues assigned to a version of a moved project | 
					
						
							| 
									
										
										
										
											2020-09-26 15:02:02 +00:00
										 |  |  |     Issue. | 
					
						
							|  |  |  |       update_versions( | 
					
						
							|  |  |  |         ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", | 
					
						
							|  |  |  |          moved_project_ids, moved_project_ids] | 
					
						
							|  |  |  |       ) | 
					
						
							| 
									
										
										
										
											2009-12-08 20:47:52 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   def parent_issue_id=(arg) | 
					
						
							| 
									
										
										
										
											2012-10-12 13:40:41 +00:00
										 |  |  |     s = arg.to_s.strip.presence | 
					
						
							|  |  |  |     if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1])) | 
					
						
							| 
									
										
										
										
											2013-10-20 09:25:14 +00:00
										 |  |  |       @invalid_parent_issue_id = nil | 
					
						
							|  |  |  |     elsif s.blank? | 
					
						
							|  |  |  |       @parent_issue = nil | 
					
						
							|  |  |  |       @invalid_parent_issue_id = nil | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |     else | 
					
						
							|  |  |  |       @parent_issue = nil | 
					
						
							| 
									
										
										
										
											2012-10-12 13:40:41 +00:00
										 |  |  |       @invalid_parent_issue_id = arg | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   def parent_issue_id | 
					
						
							| 
									
										
										
										
											2012-10-12 13:40:41 +00:00
										 |  |  |     if @invalid_parent_issue_id | 
					
						
							|  |  |  |       @invalid_parent_issue_id | 
					
						
							|  |  |  |     elsif instance_variable_defined? :@parent_issue | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |       @parent_issue.nil? ? nil : @parent_issue.id | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       parent_id | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |   def set_parent_id | 
					
						
							|  |  |  |     self.parent_id = parent_issue_id | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-02-17 10:17:10 +00:00
										 |  |  |   # Returns true if issue's project is a valid | 
					
						
							|  |  |  |   # parent issue project | 
					
						
							| 
									
										
										
										
											2012-10-10 17:38:17 +00:00
										 |  |  |   def valid_parent_project?(issue=parent) | 
					
						
							|  |  |  |     return true if issue.nil? || issue.project_id == project_id | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     case Setting.cross_project_subtasks | 
					
						
							|  |  |  |     when 'system' | 
					
						
							|  |  |  |       true | 
					
						
							|  |  |  |     when 'tree' | 
					
						
							|  |  |  |       issue.project.root == project.root | 
					
						
							|  |  |  |     when 'hierarchy' | 
					
						
							|  |  |  |       issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project) | 
					
						
							|  |  |  |     when 'descendants' | 
					
						
							|  |  |  |       issue.project.is_or_is_ancestor_of?(project) | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       false | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-09-14 10:42:31 +00:00
										 |  |  |   # Returns an issue scope based on project and scope | 
					
						
							|  |  |  |   def self.cross_project_scope(project, scope=nil) | 
					
						
							|  |  |  |     if project.nil? | 
					
						
							|  |  |  |       return Issue | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-09-14 10:42:31 +00:00
										 |  |  |     case scope | 
					
						
							|  |  |  |     when 'all', 'system' | 
					
						
							|  |  |  |       Issue | 
					
						
							|  |  |  |     when 'tree' | 
					
						
							|  |  |  |       Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)", | 
					
						
							|  |  |  |                                   :lft => project.root.lft, :rgt => project.root.rgt) | 
					
						
							|  |  |  |     when 'hierarchy' | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |       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)", | 
					
						
							|  |  |  |           :lft => project.lft, :rgt => project.rgt | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2014-09-14 10:42:31 +00:00
										 |  |  |     when 'descendants' | 
					
						
							|  |  |  |       Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)", | 
					
						
							|  |  |  |                                   :lft => project.lft, :rgt => project.rgt) | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       Issue.where(:project_id => project.id) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-03 07:16:47 +00:00
										 |  |  |   def self.by_tracker(project, with_subprojects=false) | 
					
						
							|  |  |  |     count_and_group_by(:project => project, :association => :tracker, :with_subprojects => with_subprojects) | 
					
						
							| 
									
										
										
										
											2010-02-03 16:49:21 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-03 07:16:47 +00:00
										 |  |  |   def self.by_version(project, with_subprojects=false) | 
					
						
							|  |  |  |     count_and_group_by(:project => project, :association => :fixed_version, :with_subprojects => with_subprojects) | 
					
						
							| 
									
										
										
										
											2010-02-03 16:49:21 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-03 07:16:47 +00:00
										 |  |  |   def self.by_priority(project, with_subprojects=false) | 
					
						
							|  |  |  |     count_and_group_by(:project => project, :association => :priority, :with_subprojects => with_subprojects) | 
					
						
							| 
									
										
										
										
											2010-02-03 16:49:21 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-03 07:16:47 +00:00
										 |  |  |   def self.by_category(project, with_subprojects=false) | 
					
						
							|  |  |  |     count_and_group_by(:project => project, :association => :category, :with_subprojects => with_subprojects) | 
					
						
							| 
									
										
										
										
											2010-02-03 16:49:21 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-03 07:16:47 +00:00
										 |  |  |   def self.by_assigned_to(project, with_subprojects=false) | 
					
						
							|  |  |  |     count_and_group_by(:project => project, :association => :assigned_to, :with_subprojects => with_subprojects) | 
					
						
							| 
									
										
										
										
											2010-02-03 16:49:21 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-03 07:16:47 +00:00
										 |  |  |   def self.by_author(project, with_subprojects=false) | 
					
						
							|  |  |  |     count_and_group_by(:project => project, :association => :author, :with_subprojects => with_subprojects) | 
					
						
							| 
									
										
										
										
											2010-02-03 16:49:21 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def self.by_subproject(project) | 
					
						
							| 
									
										
										
										
											2014-10-20 19:16:09 +00:00
										 |  |  |     r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project) | 
					
						
							|  |  |  |     r.reject {|r| r["project_id"] == project.id.to_s} | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Query generator for selecting groups of issue counts for a project | 
					
						
							|  |  |  |   # based on specific criteria | 
					
						
							|  |  |  |   # | 
					
						
							|  |  |  |   # Options | 
					
						
							|  |  |  |   # * project - Project to search in. | 
					
						
							|  |  |  |   # * with_subprojects - Includes subprojects issues if set to true. | 
					
						
							|  |  |  |   # * association - Symbol. Association for grouping. | 
					
						
							|  |  |  |   def self.count_and_group_by(options) | 
					
						
							|  |  |  |     assoc = reflect_on_association(options[:association]) | 
					
						
							|  |  |  |     select_field = assoc.foreign_key | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Issue. | 
					
						
							|  |  |  |       visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]). | 
					
						
							| 
									
										
										
										
											2014-10-20 21:47:43 +00:00
										 |  |  |       joins(:status, assoc.name). | 
					
						
							| 
									
										
										
										
											2014-10-20 19:16:09 +00:00
										 |  |  |       group(:status_id, :is_closed, select_field). | 
					
						
							|  |  |  |       count. | 
					
						
							|  |  |  |       map do |columns, total| | 
					
						
							|  |  |  |         status_id, is_closed, field_value = columns | 
					
						
							| 
									
										
										
										
											2014-10-21 18:44:33 +00:00
										 |  |  |         is_closed = ['t', 'true', '1'].include?(is_closed.to_s) | 
					
						
							| 
									
										
										
										
											2014-10-20 19:16:09 +00:00
										 |  |  |         { | 
					
						
							|  |  |  |           "status_id" => status_id.to_s, | 
					
						
							| 
									
										
										
										
											2014-10-21 17:47:18 +00:00
										 |  |  |           "closed" => is_closed, | 
					
						
							| 
									
										
										
										
											2014-10-20 19:16:09 +00:00
										 |  |  |           select_field => field_value.to_s, | 
					
						
							|  |  |  |           "total" => total.to_s | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-06-11 16:54:48 +00:00
										 |  |  |   # Returns a scope of projects that user can assign the issue to | 
					
						
							| 
									
										
										
										
											2018-02-28 06:42:52 +00:00
										 |  |  |   def allowed_target_projects(user=User.current, context=nil) | 
					
						
							|  |  |  |     if new_record? && context.is_a?(Project) && !copy? | 
					
						
							|  |  |  |       current_project = context.self_and_descendants | 
					
						
							|  |  |  |     elsif new_record? | 
					
						
							|  |  |  |       current_project = nil | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       current_project = project | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-08 10:20:53 +00:00
										 |  |  |     self.class.allowed_target_projects(user, current_project) | 
					
						
							| 
									
										
										
										
											2012-01-08 11:48:36 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-08 10:20:53 +00:00
										 |  |  |   # Returns a scope of projects that user can assign issues to | 
					
						
							|  |  |  |   # If current_project is given, it will be included in the scope | 
					
						
							|  |  |  |   def self.allowed_target_projects(user=User.current, current_project=nil) | 
					
						
							|  |  |  |     condition = Project.allowed_to_condition(user, :add_issues) | 
					
						
							| 
									
										
										
										
											2018-02-28 06:42:52 +00:00
										 |  |  |     if current_project.is_a?(Project) | 
					
						
							| 
									
										
										
										
											2015-02-08 10:20:53 +00:00
										 |  |  |       condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id] | 
					
						
							| 
									
										
										
										
											2018-02-28 06:42:52 +00:00
										 |  |  |     elsif current_project | 
					
						
							|  |  |  |       condition = ["(#{condition}) AND #{Project.table_name}.id IN (?)", current_project.map(&:id)] | 
					
						
							| 
									
										
										
										
											2015-02-08 10:20:53 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2015-09-20 11:38:01 +00:00
										 |  |  |     Project.where(condition).having_trackers | 
					
						
							| 
									
										
										
										
											2010-04-18 12:47:41 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2016-10-01 07:43:22 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-30 18:20:13 +00:00
										 |  |  |   # Returns a scope of trackers that user can assign the issue to | 
					
						
							|  |  |  |   def allowed_target_trackers(user=User.current) | 
					
						
							| 
									
										
										
										
											2016-06-05 10:06:17 +00:00
										 |  |  |     self.class.allowed_target_trackers(project, user, tracker_id_was) | 
					
						
							| 
									
										
										
										
											2016-05-30 18:20:13 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Returns a scope of trackers that user can assign project issues to | 
					
						
							|  |  |  |   def self.allowed_target_trackers(project, user=User.current, current_tracker=nil) | 
					
						
							| 
									
										
										
										
											2016-06-05 10:06:17 +00:00
										 |  |  |     if project | 
					
						
							|  |  |  |       scope = project.trackers.sorted | 
					
						
							|  |  |  |       unless user.admin? | 
					
						
							|  |  |  |         roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)} | 
					
						
							|  |  |  |         unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)} | 
					
						
							|  |  |  |           tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq | 
					
						
							|  |  |  |           if current_tracker | 
					
						
							|  |  |  |             tracker_ids << current_tracker | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |           scope = scope.where(:id => tracker_ids) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |       scope | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       Tracker.none | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-05-30 18:20:13 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-12-08 20:47:52 +00:00
										 |  |  |   private | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-06-05 13:45:10 +00:00
										 |  |  |   def user_tracker_permission?(user, permission) | 
					
						
							| 
									
										
										
										
											2016-10-02 11:00:08 +00:00
										 |  |  |     if project && !project.active? | 
					
						
							|  |  |  |       perm = Redmine::AccessControl.permission(permission) | 
					
						
							|  |  |  |       return false unless perm && perm.read? | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-06-06 06:32:10 +00:00
										 |  |  |     if user.admin? | 
					
						
							|  |  |  |       true | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)} | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |       roles.any? do |r| | 
					
						
							|  |  |  |         r.permissions_all_trackers?(permission) || | 
					
						
							|  |  |  |           r.permissions_tracker_ids?(permission, tracker_id) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-06-06 06:32:10 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-06-05 13:45:10 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |   def after_project_change | 
					
						
							|  |  |  |     # Update project_id on related time entries | 
					
						
							| 
									
										
										
										
											2014-01-08 12:20:52 +00:00
										 |  |  |     TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id]) | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # Delete issue relations | 
					
						
							|  |  |  |     unless Setting.cross_project_issue_relations? | 
					
						
							|  |  |  |       relations_from.clear | 
					
						
							|  |  |  |       relations_to.clear | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-10-10 17:38:17 +00:00
										 |  |  |     # Move subtasks that were in the same project | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |     children.each do |child| | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |       next unless child.project_id == project_id_before_last_save | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-01-07 12:34:52 +00:00
										 |  |  |       # Change project and keep project | 
					
						
							|  |  |  |       child.send :project=, project, true | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |       unless child.save | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |         errors.add( | 
					
						
							|  |  |  |           :base, | 
					
						
							|  |  |  |           l(:error_move_of_child_not_possible, | 
					
						
							|  |  |  |             :child => "##{child.id}", | 
					
						
							|  |  |  |             :errors => child.errors.full_messages.join(", ")) | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2012-01-06 19:50:02 +00:00
										 |  |  |         raise ActiveRecord::Rollback | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-09-27 19:09:30 +00:00
										 |  |  |   # Callback for after the creation of an issue by copy | 
					
						
							|  |  |  |   # * adds a "copied to" relation with the copied issue | 
					
						
							|  |  |  |   # * copies subtasks from the copied issue | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |   def after_create_from_copy | 
					
						
							| 
									
										
										
										
											2012-09-27 19:09:30 +00:00
										 |  |  |     return unless copy? && !@after_create_from_copy_handled | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |     if (@copied_from.project_id == project_id || | 
					
						
							|  |  |  |           Setting.cross_project_issue_relations?) && | 
					
						
							|  |  |  |         @copy_options[:link] != false | 
					
						
							| 
									
										
										
										
											2014-11-02 15:38:11 +00:00
										 |  |  |       if @current_journal | 
					
						
							|  |  |  |         @copied_from.init_journal(@current_journal.user) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |       relation = | 
					
						
							|  |  |  |         IssueRelation.new(:issue_from => @copied_from, :issue_to => self, | 
					
						
							|  |  |  |                           :relation_type => IssueRelation::TYPE_COPIED_TO) | 
					
						
							| 
									
										
										
										
											2012-09-27 19:09:30 +00:00
										 |  |  |       unless relation.save | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |         if logger | 
					
						
							|  |  |  |           logger.error( | 
					
						
							|  |  |  |             "Could not create relation while copying ##{@copied_from.id} to ##{id} " \ | 
					
						
							|  |  |  |               "due to validation errors: #{relation.errors.full_messages.join(', ')}" | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2012-09-27 19:09:30 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     unless @copied_from.leaf? || @copy_options[:subtasks] == false | 
					
						
							| 
									
										
										
										
											2013-03-01 12:26:06 +00:00
										 |  |  |       copy_options = (@copy_options || {}).merge(:subtasks => false) | 
					
						
							|  |  |  |       copied_issue_ids = {@copied_from.id => self.id} | 
					
						
							|  |  |  |       @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child| | 
					
						
							|  |  |  |         # Do not copy self when copying an issue as a descendant of the copied issue | 
					
						
							|  |  |  |         next if child == self | 
					
						
							|  |  |  |         # Do not copy subtasks of issues that were not copied | 
					
						
							|  |  |  |         next unless copied_issue_ids[child.parent_id] | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-03-01 12:26:06 +00:00
										 |  |  |         # Do not copy subtasks that are not visible to avoid potential disclosure of private data | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |         unless child.visible? | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |           if logger | 
					
						
							|  |  |  |             logger.error( | 
					
						
							|  |  |  |               "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy " \ | 
					
						
							|  |  |  |                 "because it is not visible to the current user" | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |           next | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2013-03-01 12:26:06 +00:00
										 |  |  |         copy = Issue.new.copy_from(child, copy_options) | 
					
						
							| 
									
										
										
										
											2014-11-02 15:38:11 +00:00
										 |  |  |         if @current_journal | 
					
						
							|  |  |  |           copy.init_journal(@current_journal.user) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |         copy.author = author | 
					
						
							|  |  |  |         copy.project = project | 
					
						
							| 
									
										
										
										
											2013-03-01 12:26:06 +00:00
										 |  |  |         copy.parent_issue_id = copied_issue_ids[child.parent_id] | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |         unless child.fixed_version.present? && child.fixed_version.status == 'open' | 
					
						
							|  |  |  |           copy.fixed_version_id = nil | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |         unless child.assigned_to_id.present? && | 
					
						
							|  |  |  |                  child.assigned_to.status == User::STATUS_ACTIVE | 
					
						
							|  |  |  |           copy.assigned_to = nil | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |         unless copy.save | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |           if logger | 
					
						
							|  |  |  |             logger.error( | 
					
						
							|  |  |  |               "Could not copy subtask ##{child.id} " \ | 
					
						
							|  |  |  |                 "while copying ##{@copied_from.id} to ##{id} due to validation errors: " \ | 
					
						
							|  |  |  |                 "#{copy.errors.full_messages.join(', ')}" | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2013-03-01 12:26:06 +00:00
										 |  |  |           next | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2013-03-01 12:26:06 +00:00
										 |  |  |         copied_issue_ids[child.id] = copy.id | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2012-09-27 19:09:30 +00:00
										 |  |  |     @after_create_from_copy_handled = true | 
					
						
							| 
									
										
										
										
											2012-09-08 05:34:07 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   def update_nested_set_attributes | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |     if saved_change_to_parent_id? | 
					
						
							| 
									
										
										
										
											2013-10-20 09:25:14 +00:00
										 |  |  |       update_nested_set_attributes_on_parent_change | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Updates the nested set for when an existing issue is moved | 
					
						
							|  |  |  |   def update_nested_set_attributes_on_parent_change | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |     former_parent_id = parent_id_before_last_save | 
					
						
							| 
									
										
										
										
											2013-10-20 09:25:14 +00:00
										 |  |  |     # delete invalid relations of all descendants | 
					
						
							|  |  |  |     self_and_descendants.each do |issue| | 
					
						
							|  |  |  |       issue.relations.each do |relation| | 
					
						
							|  |  |  |         relation.destroy unless relation.valid? | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2013-10-20 09:25:14 +00:00
										 |  |  |     # update former parent | 
					
						
							|  |  |  |     recalculate_attributes_for(former_parent_id) if former_parent_id | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |   def update_parent_attributes | 
					
						
							| 
									
										
										
										
											2015-01-07 20:19:49 +00:00
										 |  |  |     if parent_id | 
					
						
							|  |  |  |       recalculate_attributes_for(parent_id) | 
					
						
							|  |  |  |       association(:parent).reset | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2010-06-30 02:45:34 +00:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def recalculate_attributes_for(issue_id) | 
					
						
							|  |  |  |     if issue_id && p = Issue.find_by_id(issue_id) | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |       if p.priority_derived? | 
					
						
							| 
									
										
										
										
											2016-01-16 09:00:37 +00:00
										 |  |  |         # priority = highest priority of open children | 
					
						
							| 
									
										
										
										
											2016-10-01 09:38:58 +00:00
										 |  |  |         # priority is left unchanged if all children are closed and there's no default priority defined | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |         if priority_position = | 
					
						
							|  |  |  |              p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position") | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |           p.priority = IssuePriority.find_by_position(priority_position) | 
					
						
							| 
									
										
										
										
											2016-10-01 09:38:58 +00:00
										 |  |  |         elsif default_priority = IssuePriority.default | 
					
						
							|  |  |  |           p.priority = default_priority | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-05-25 09:53:05 +00:00
										 |  |  |       if p.dates_derived? | 
					
						
							|  |  |  |         # start/due dates = lowest/highest dates of children | 
					
						
							|  |  |  |         p.start_date = p.children.minimum(:start_date) | 
					
						
							|  |  |  |         p.due_date = p.children.maximum(:due_date) | 
					
						
							|  |  |  |         if p.start_date && p.due_date && p.due_date < p.start_date | 
					
						
							|  |  |  |           p.start_date, p.due_date = p.due_date, p.start_date | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-05-25 10:03:42 +00:00
										 |  |  |       if p.done_ratio_derived? | 
					
						
							| 
									
										
										
										
											2016-09-01 17:03:17 +00:00
										 |  |  |         # done ratio = average ratio of children weighted with their total estimated hours | 
					
						
							| 
									
										
										
										
											2015-05-25 10:03:42 +00:00
										 |  |  |         unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio | 
					
						
							| 
									
										
										
										
											2016-09-01 17:03:17 +00:00
										 |  |  |           children = p.children.to_a | 
					
						
							|  |  |  |           if children.any? | 
					
						
							|  |  |  |             child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0} | 
					
						
							|  |  |  |             if child_with_total_estimated_hours.any? | 
					
						
							| 
									
										
										
										
											2020-12-17 13:11:05 +00:00
										 |  |  |               average = | 
					
						
							|  |  |  |                 child_with_total_estimated_hours.sum(&:total_estimated_hours).to_d / | 
					
						
							|  |  |  |                   child_with_total_estimated_hours.count | 
					
						
							| 
									
										
										
										
											2016-09-01 17:03:17 +00:00
										 |  |  |             else | 
					
						
							| 
									
										
										
										
											2020-06-14 06:56:05 +00:00
										 |  |  |               average = 1.0.to_d | 
					
						
							| 
									
										
										
										
											2015-05-25 10:03:42 +00:00
										 |  |  |             end | 
					
						
							| 
									
										
										
										
											2020-12-06 01:36:39 +00:00
										 |  |  |             done = children.sum do |c| | 
					
						
							| 
									
										
										
										
											2020-06-14 06:56:05 +00:00
										 |  |  |               estimated = (c.total_estimated_hours || 0.0).to_d | 
					
						
							| 
									
										
										
										
											2016-09-01 17:03:17 +00:00
										 |  |  |               estimated = average unless estimated > 0.0
 | 
					
						
							|  |  |  |               ratio = c.closed? ? 100 : (c.done_ratio || 0) | 
					
						
							|  |  |  |               estimated * ratio | 
					
						
							| 
									
										
										
										
											2020-12-06 01:36:39 +00:00
										 |  |  |             end | 
					
						
							| 
									
										
										
										
											2016-09-01 17:03:17 +00:00
										 |  |  |             progress = done / (average * children.count) | 
					
						
							| 
									
										
										
										
											2018-01-08 22:35:59 +00:00
										 |  |  |             p.done_ratio = progress.floor | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |       # ancestors will be recursively updated | 
					
						
							| 
									
										
										
										
											2012-04-25 17:17:49 +00:00
										 |  |  |       p.save(:validate => false) | 
					
						
							| 
									
										
										
										
											2010-03-13 14:56:49 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-19 13:33:43 +00:00
										 |  |  |   # Singleton class method is public | 
					
						
							|  |  |  |   class << self | 
					
						
							|  |  |  |     # Update issues so their versions are not pointing to a | 
					
						
							|  |  |  |     # fixed_version that is not shared with the issue's project | 
					
						
							|  |  |  |     def update_versions(conditions=nil) | 
					
						
							|  |  |  |       # Only need to update issues with a fixed_version from | 
					
						
							|  |  |  |       # a different project and that is not systemwide shared | 
					
						
							|  |  |  |       Issue.joins(:project, :fixed_version). | 
					
						
							|  |  |  |         where("#{Issue.table_name}.fixed_version_id IS NOT NULL" + | 
					
						
							|  |  |  |           " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + | 
					
						
							|  |  |  |           " AND #{Version.table_name}.sharing <> 'system'"). | 
					
						
							|  |  |  |         where(conditions).each do |issue| | 
					
						
							|  |  |  |         next if issue.project.nil? || issue.fixed_version.nil? | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-19 13:33:43 +00:00
										 |  |  |         unless issue.project.shared_versions.include?(issue.fixed_version) | 
					
						
							|  |  |  |           issue.init_journal(User.current) | 
					
						
							|  |  |  |           issue.fixed_version = nil | 
					
						
							|  |  |  |           issue.save | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2009-12-06 10:28:20 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-08-21 08:46:10 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-07-13 20:04:14 +00:00
										 |  |  |   def delete_selected_attachments | 
					
						
							|  |  |  |     if deleted_attachment_ids.present? | 
					
						
							|  |  |  |       objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i)) | 
					
						
							|  |  |  |       attachments.delete(objects) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-09-12 21:30:30 +00:00
										 |  |  |   # Callback on file attachment | 
					
						
							| 
									
										
										
										
											2014-12-05 08:03:32 +00:00
										 |  |  |   def attachment_added(attachment) | 
					
						
							|  |  |  |     if current_journal && !attachment.new_record? | 
					
						
							|  |  |  |       current_journal.journalize_attachment(attachment, :added) | 
					
						
							| 
									
										
										
										
											2011-07-24 09:34:23 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-12-09 16:54:46 +00:00
										 |  |  |   # Callback on attachment deletion | 
					
						
							| 
									
										
										
										
											2014-12-05 08:03:32 +00:00
										 |  |  |   def attachment_removed(attachment) | 
					
						
							|  |  |  |     if current_journal && !attachment.new_record? | 
					
						
							|  |  |  |       current_journal.journalize_attachment(attachment, :removed) | 
					
						
							|  |  |  |       current_journal.save | 
					
						
							| 
									
										
										
										
											2012-03-04 11:41:10 +00:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2008-12-09 16:54:46 +00:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2014-11-02 15:38:11 +00:00
										 |  |  |   # Called after a relation is added | 
					
						
							|  |  |  |   def relation_added(relation) | 
					
						
							| 
									
										
										
										
											2014-12-05 08:03:32 +00:00
										 |  |  |     if current_journal | 
					
						
							|  |  |  |       current_journal.journalize_relation(relation, :added) | 
					
						
							|  |  |  |       current_journal.save | 
					
						
							| 
									
										
										
										
											2014-11-02 15:38:11 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Called after a relation is removed | 
					
						
							|  |  |  |   def relation_removed(relation) | 
					
						
							| 
									
										
										
										
											2014-12-05 08:03:32 +00:00
										 |  |  |     if current_journal | 
					
						
							|  |  |  |       current_journal.journalize_relation(relation, :removed) | 
					
						
							|  |  |  |       current_journal.save | 
					
						
							| 
									
										
										
										
											2014-11-02 15:38:11 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-08 08:02:06 +00:00
										 |  |  |   # Default assignment based on project or category | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |   def default_assign | 
					
						
							| 
									
										
										
										
											2017-04-08 08:02:06 +00:00
										 |  |  |     if assigned_to.nil? | 
					
						
							|  |  |  |       if category && category.assigned_to | 
					
						
							|  |  |  |         self.assigned_to = category.assigned_to | 
					
						
							|  |  |  |       elsif project && project.default_assigned_to | 
					
						
							|  |  |  |         self.assigned_to = project.default_assigned_to | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Updates start/due dates of following issues | 
					
						
							|  |  |  |   def reschedule_following_issues | 
					
						
							| 
									
										
										
										
											2017-07-23 11:26:04 +00:00
										 |  |  |     if saved_change_to_start_date? || saved_change_to_due_date? | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |       relations_from.each do |relation| | 
					
						
							| 
									
										
										
										
											2018-06-10 05:57:12 +00:00
										 |  |  |         relation.set_issue_to_dates(@current_journal) | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Closes duplicates if the issue is being closed | 
					
						
							|  |  |  |   def close_duplicates | 
					
						
							| 
									
										
										
										
											2017-06-25 11:40:58 +00:00
										 |  |  |     if Setting.close_duplicate_issues? && closing? | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |       duplicates.each do |duplicate| | 
					
						
							| 
									
										
										
										
											2014-04-07 08:07:08 +00:00
										 |  |  |         # Reload is needed in case the duplicate was updated by a previous duplicate | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |         duplicate.reload | 
					
						
							|  |  |  |         # Don't re-close it if it's already closed | 
					
						
							|  |  |  |         next if duplicate.closed? | 
					
						
							| 
									
										
										
										
											2020-07-11 16:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |         # Same user and notes | 
					
						
							|  |  |  |         if @current_journal | 
					
						
							|  |  |  |           duplicate.init_journal(@current_journal.user, @current_journal.notes) | 
					
						
							| 
									
										
										
										
											2016-03-12 08:21:32 +00:00
										 |  |  |           duplicate.private_notes = @current_journal.private_notes | 
					
						
							| 
									
										
										
										
											2010-02-28 09:21:12 +00:00
										 |  |  |         end | 
					
						
							|  |  |  |         duplicate.update_attribute :status, self.status | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2011-05-17 02:14:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-02-16 09:38:01 +00:00
										 |  |  |   # Make sure updated_on is updated when adding a note and set updated_on now | 
					
						
							|  |  |  |   # so we can set closed_on with the same value on closing | 
					
						
							| 
									
										
										
										
											2012-05-21 18:23:03 +00:00
										 |  |  |   def force_updated_on_change | 
					
						
							| 
									
										
										
										
											2013-02-16 09:38:01 +00:00
										 |  |  |     if @current_journal || changed? | 
					
						
							| 
									
										
										
										
											2012-05-21 18:23:03 +00:00
										 |  |  |       self.updated_on = current_time_from_proper_timezone | 
					
						
							| 
									
										
										
										
											2013-02-16 09:38:01 +00:00
										 |  |  |       if new_record? | 
					
						
							|  |  |  |         self.created_on = updated_on | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Callback for setting closed_on when the issue is closed. | 
					
						
							|  |  |  |   # The closed_on attribute stores the time of the last closing | 
					
						
							|  |  |  |   # and is preserved when the issue is reopened. | 
					
						
							|  |  |  |   def update_closed_on | 
					
						
							| 
									
										
										
										
											2014-10-25 09:35:17 +00:00
										 |  |  |     if closing? | 
					
						
							| 
									
										
										
										
											2013-02-16 09:38:01 +00:00
										 |  |  |       self.closed_on = updated_on | 
					
						
							| 
									
										
										
										
											2012-05-21 18:23:03 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2009-04-08 19:11:30 +00:00
										 |  |  |   # Saves the changes in a Journal | 
					
						
							|  |  |  |   # Called after_save | 
					
						
							|  |  |  |   def create_journal | 
					
						
							| 
									
										
										
										
											2014-12-05 08:03:32 +00:00
										 |  |  |     if current_journal | 
					
						
							|  |  |  |       current_journal.save | 
					
						
							| 
									
										
										
										
											2009-04-08 19:11:30 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2010-02-04 17:24:33 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-07-14 14:26:27 +00:00
										 |  |  |   def send_notification | 
					
						
							| 
									
										
										
										
											2015-08-14 08:20:32 +00:00
										 |  |  |     if notify? && Setting.notified_events.include?('issue_added') | 
					
						
							| 
									
										
										
										
											2013-07-14 14:26:27 +00:00
										 |  |  |       Mailer.deliver_issue_add(self) | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-04-14 17:16:52 +00:00
										 |  |  |   def clear_disabled_fields | 
					
						
							|  |  |  |     if tracker | 
					
						
							|  |  |  |       tracker.disabled_core_fields.each do |attribute| | 
					
						
							|  |  |  |         send "#{attribute}=", nil | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2015-05-09 09:48:11 +00:00
										 |  |  |       self.done_ratio ||= 0
 | 
					
						
							| 
									
										
										
										
											2015-04-14 17:16:52 +00:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2006-06-28 18:11:03 +00:00
										 |  |  | end |