2011-02-27 13:34:41 +00:00
# Redmine - project management software
2017-06-25 08:40:31 +00:00
# Copyright (C) 2006-2017 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 } } ,
2016-11-19 08:26:58 +00:00
:type = > Proc . new { | o | 'issue' + ( o . closed? ? '-closed' : '' ) }
2011-05-17 02:14:06 +00:00
2016-06-18 06:51:59 +00:00
acts_as_activity_provider :scope = > 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
2016-07-13 20:04:14 +00:00
attr_accessor :deleted_attachment_ids
2010-03-04 16:18:51 +00:00
attr_reader :current_journal
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
2012-12-07 17:59:20 +00:00
scope :visible , lambda { | * 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 ) )
2012-12-07 17:59:20 +00:00
}
2011-05-17 02:14:06 +00:00
2012-04-26 23:51:10 +00:00
scope :open , lambda { | * args |
2011-12-18 13:05:46 +00:00
is_closed = args . size > 0 ? ! 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 } )
2011-12-18 13:05:46 +00:00
}
2009-12-11 18:48:34 +00:00
2017-01-21 10:09:13 +00:00
scope :recently_updated , lambda { order ( :updated_on = > :desc ) }
2012-12-07 17:59:20 +00:00
scope :on_active_project , lambda {
2014-10-22 17:37:16 +00:00
joins ( :project ) .
2017-01-21 10:09:13 +00:00
where ( :projects = > { :status = > Project :: STATUS_ACTIVE } )
2012-12-07 17:59:20 +00:00
}
2013-01-06 14:03:49 +00:00
scope :fixed_version , lambda { | versions |
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
2013-01-06 14:03:49 +00:00
}
2015-10-21 17:00:14 +00:00
scope :assigned_to , lambda { | arg |
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
}
2017-01-21 10:05:53 +00:00
scope :like , lambda { | q |
q = q . to_s
if q . present?
where ( " LOWER( #{ table_name } .subject) LIKE LOWER(?) " , " % #{ q } % " )
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
after_save { | issue | issue . send :after_project_change if ! issue . saved_change_to_id? && issue . saved_change_to_project_id? }
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
2013-07-14 14:26:27 +00:00
after_create :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 |
2016-06-05 11:50:09 +00:00
sql = if user . id && user . logged?
2012-09-20 19:26:58 +00:00
case role . issues_visibility
when 'all'
2016-06-05 11:50:09 +00:00
'1=1'
2012-09-20 19:26:58 +00:00
when 'default'
2013-09-25 17:35:59 +00:00
user_ids = [ user . id ] + user . groups . map ( & :id ) . compact
2012-09-19 21:48:33 +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 ( ',' ) } )) "
2012-09-20 19:26:58 +00:00
when 'own'
2013-09-25 17:35:59 +00:00
user_ids = [ user . id ] + user . groups . map ( & :id ) . compact
2012-09-19 21:48:33 +00:00
" ( #{ table_name } .author_id = #{ user . id } OR #{ table_name } .assigned_to_id IN ( #{ user_ids . join ( ',' ) } )) "
else
'1=0'
end
2011-04-11 17:53:15 +00:00
else
2012-09-20 19:26:58 +00:00
" ( #{ table_name } .is_private = #{ connection . quoted_false } ) "
2011-04-11 17:53:15 +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 |
2016-06-05 11:50:09 +00:00
visible = if user . logged?
2012-09-20 19:26:58 +00:00
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
2011-04-11 17:53:15 +00:00
else
2012-09-20 19:26:58 +00:00
! self . is_private?
2011-04-11 17:53:15 +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 )
2016-06-05 13:45:10 +00:00
user_tracker_permission? ( user , :edit_issues )
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
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 )
2017-04-03 10:30:29 +00:00
self . attributes = issue . attributes . dup . except ( " id " , " root_id " , " parent_id " , " lft " , " rgt " , " created_on " , " updated_on " , " status_id " , " closed_on " )
2010-03-13 14:56:49 +00:00
self . custom_field_values = issue . custom_field_values . inject ( { } ) { | h , v | h [ v . custom_field_id ] = v . value ; h }
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?
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
2012-01-07 12:34:52 +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' ,
2012-10-03 21:36:19 +00:00
'notes' ,
2016-06-05 13:45:10 +00:00
:if = > lambda { | issue , user | issue . new_record? || issue . attributes_editable? ( user ) }
2011-05-17 02:14:06 +00:00
2012-10-03 21:36:19 +00:00
safe_attributes 'notes' ,
2016-06-05 13:45:10 +00:00
:if = > lambda { | issue , user | issue . notes_addable? ( user ) }
2012-10-03 21:36:19 +00:00
safe_attributes 'private_notes' ,
2014-04-08 11:22:33 +00:00
:if = > lambda { | issue , user | ! issue . new_record? && user . allowed_to? ( :set_notes_private , issue . project ) }
2012-10-03 21:36:19 +00:00
2011-12-13 19:50:44 +00:00
safe_attributes 'watcher_user_ids' ,
2014-04-08 11:22:33 +00:00
:if = > lambda { | issue , user | issue . new_record? && user . allowed_to? ( :add_issue_watchers , issue . project ) }
2011-12-13 19:50:44 +00:00
2011-04-15 13:23:13 +00:00
safe_attributes 'is_private' ,
:if = > lambda { | issue , user |
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 ) )
2011-04-15 13:23:13 +00:00
}
2011-05-17 02:14:06 +00:00
2011-12-13 19:56:33 +00:00
safe_attributes 'parent_issue_id' ,
2016-06-05 13:45:10 +00:00
:if = > lambda { | issue , user | ( issue . new_record? || issue . attributes_editable? ( user ) ) &&
2011-12-13 19:56:33 +00:00
user . allowed_to? ( :manage_subtasks , issue . project ) }
2016-07-13 20:04:14 +00:00
safe_attributes 'deleted_attachment_ids' ,
:if = > lambda { | issue , user | issue . attachments_deletable? ( user ) }
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' )
2016-10-01 09:06:17 +00:00
if p . is_a? ( String ) && ! p . match ( / ^ \ d*$ / )
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
if project_id_changed? && attrs [ 'category_id' ] . to_s == category_id_was . to_s
# 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
2011-05-17 02:14:06 +00:00
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 = { }
2014-10-26 22:20:10 +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 = { }
IssueCustomField . where ( :visible = > false ) . joins ( :roles ) . pluck ( :id , " role_id " ) . each do | field_id , role_id |
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
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
2009-11-08 13:03:41 +00:00
if fixed_version
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
2016-12-10 12:02:37 +00:00
if assigned_to_id_changed? && assigned_to_id . present?
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 ) ||
@parent_issue . self_and_ancestors . any? { | a | a . relations_from . any? { | r | r . relation_type == IssueRelation :: TYPE_PRECEDES && r . issue_to . would_reschedule? ( self ) } }
)
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 |
if attribute =~ / ^ \ d+$ /
attribute = attribute . to_i
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?
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
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?
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
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
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
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
2016-12-21 09:09:22 +00:00
if blocked? || descendants . open . any?
# 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
2016-12-21 09:09:22 +00:00
if ancestors . open ( false ) . any?
# 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
# This is how ActiveRecord::AttributeMethods::Dirty checks if we're in a after_* callback
if previous_assigned_to_id = mutation_tracker . equal? ( mutations_from_database ) ? assigned_to_id_in_database : assigned_to_id_before_last_save
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
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
2013-07-13 09:20:11 +00:00
def each_notification ( users , & block )
if users . any?
if custom_field_values . detect { | value | ! value . custom_field . visible? }
users_by_custom_field_visibility = users . group_by do | user |
visible_custom_field_values ( user ) . map ( & :custom_field_id ) . sort
end
users_by_custom_field_visibility . values . each do | users |
yield ( users )
end
else
yield ( users )
end
end
end
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
2015-07-05 12:38:39 +00:00
@total_spent_hours || = if leaf?
2015-05-25 12:09:01 +00:00
spent_hours
else
2015-07-05 12:38:39 +00:00
self_and_descendants . joins ( :time_entries ) . sum ( " #{ TimeEntry . table_name } .hours " ) . to_f || 0 . 0
2015-05-25 12:09:01 +00:00
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
@total_estimated_hours || = self_and_descendants . sum ( :estimated_hours )
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?
2013-06-12 19:13:25 +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 |
issue . instance_variable_set " @relations " , relations . select { | r | r . issue_from_id == issue . id || r . issue_to_id == issue . id }
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 )
2016-12-31 16:30:33 +00:00
Issue . joins ( " JOIN #{ Issue . table_name } ancestors " +
2016-12-31 15:04:05 +00:00
" ON ancestors.root_id = #{ Issue . table_name } .root_id " +
" AND ancestors.lft <= #{ Issue . table_name } .lft AND ancestors.rgt >= #{ Issue . table_name } .rgt "
) .
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?
current = last . map { | i | i . relations_from . where ( :relation_type = > IssueRelation :: TYPE_BLOCKS ) . map ( & :issue_to ) } . flatten . uniq
current -= last
current -= all
return true if current . include? ( other )
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?
current = last . map { | i |
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 ) }
} . flatten . uniq
current -= last
current -= all
return true if current . include? ( other )
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.
def reschedule_on! ( date )
2010-03-13 15:29:34 +00:00
return if date . nil?
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
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 )
2013-03-01 10:42:05 +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?
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 )
moved_project_ids = project . self_and_descendants . reload . collect ( & :id )
# Update issues of the moved projects and issues assigned to a version of a moved project
2014-04-08 11:22:17 +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
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'
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 )
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
2010-02-03 16:49:21 +00:00
def self . by_tracker ( project )
2014-10-20 19:16:09 +00:00
count_and_group_by ( :project = > project , :association = > :tracker )
2010-02-03 16:49:21 +00:00
end
def self . by_version ( project )
2014-10-20 19:16:09 +00:00
count_and_group_by ( :project = > project , :association = > :fixed_version )
2010-02-03 16:49:21 +00:00
end
def self . by_priority ( project )
2014-10-20 19:16:09 +00:00
count_and_group_by ( :project = > project , :association = > :priority )
2010-02-03 16:49:21 +00:00
end
def self . by_category ( project )
2014-10-20 19:16:09 +00:00
count_and_group_by ( :project = > project , :association = > :category )
2010-02-03 16:49:21 +00:00
end
def self . by_assigned_to ( project )
2014-10-20 19:16:09 +00:00
count_and_group_by ( :project = > project , :association = > :assigned_to )
2010-02-03 16:49:21 +00:00
end
def self . by_author ( project )
2014-10-20 19:16:09 +00:00
count_and_group_by ( :project = > project , :association = > :author )
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 ) }
roles . any? { | r | r . permissions_all_trackers? ( permission ) || r . permissions_tracker_ids? ( permission , tracker_id ) }
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
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
2016-12-18 08:54:26 +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
2012-09-27 22:27:37 +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
2012-09-27 19:09:30 +00:00
relation = IssueRelation . new ( :issue_from = > @copied_from , :issue_to = > self , :relation_type = > IssueRelation :: TYPE_COPIED_TO )
unless relation . save
logger . error " Could not create relation while copying # #{ @copied_from . id } to # #{ id } due to validation errors: #{ relation . errors . full_messages . join ( ', ' ) } " if logger
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 ]
# 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?
logger . error " Subtask # #{ child . id } was not copied during # #{ @copied_from . id } copy because it is not visible to the current user " if logger
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 ]
2018-06-07 00:33:29 +00:00
copy . fixed_version_id = nil unless child . fixed_version . present? && child . fixed_version . status == 'open'
2012-09-08 05:34:07 +00:00
unless copy . save
logger . error " Could not copy subtask # #{ child . id } while copying # #{ @copied_from . id } to # #{ id } due to validation errors: #{ copy . errors . full_messages . join ( ', ' ) } " if logger
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
2016-01-16 09:00:37 +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?
average = child_with_total_estimated_hours . map ( & :total_estimated_hours ) . sum . to_f / child_with_total_estimated_hours . count
else
average = 1 . 0
2015-05-25 10:03:42 +00:00
end
2016-09-01 17:03:17 +00:00
done = children . map { | c |
estimated = c . total_estimated_hours . to_f
estimated = average unless estimated > 0 . 0
ratio = c . closed? ? 100 : ( c . done_ratio || 0 )
estimated * ratio
} . sum
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
2009-12-08 20:47:52 +00:00
# Update issues so their versions are not pointing to a
# fixed_version that is not shared with the issue's project
def self . update_versions ( conditions = nil )
# Only need to update issues with a fixed_version from
# a different project and that is not systemwide shared
2014-10-22 17:37:16 +00:00
Issue . joins ( :project , :fixed_version ) .
2013-06-12 19:13:25 +00:00
where ( " #{ Issue . table_name } .fixed_version_id IS NOT NULL " +
2011-12-18 15:02:46 +00:00
" AND #{ Issue . table_name } .project_id <> #{ Version . table_name } .project_id " +
2013-06-12 19:13:25 +00:00
" AND #{ Version . table_name } .sharing <> 'system' " ) .
where ( conditions ) . each do | issue |
2009-12-06 10:28:20 +00:00
next if issue . project . nil? || issue . fixed_version . nil?
unless issue . project . shared_versions . include? ( issue . fixed_version )
issue . init_journal ( User . current )
issue . fixed_version = nil
issue . save
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 |
relation . set_issue_to_dates
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?
# 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