2011-02-27 16:34:41 +03:00
# Redmine - project management software
2012-05-05 16:56:53 +04:00
# Copyright (C) 2006-2012 Jean-Philippe Lang
2007-03-12 20:59:02 +03: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 06:14:06 +04:00
#
2007-03-12 20:59:02 +03: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 06:14:06 +04:00
#
2007-03-12 20:59:02 +03: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 16:11:53 +03:00
include Redmine :: SafeAttributes
2012-10-29 14:06:30 +04:00
include Redmine :: Utils :: DateCalculation
2011-05-17 06:14:06 +04:00
2007-03-12 20:59:02 +03:00
belongs_to :project
belongs_to :tracker
belongs_to :status , :class_name = > 'IssueStatus' , :foreign_key = > 'status_id'
belongs_to :author , :class_name = > 'User' , :foreign_key = > 'author_id'
2011-07-23 22:18:13 +04:00
belongs_to :assigned_to , :class_name = > 'Principal' , :foreign_key = > 'assigned_to_id'
2007-03-12 20:59:02 +03:00
belongs_to :fixed_version , :class_name = > 'Version' , :foreign_key = > 'fixed_version_id'
2009-05-31 03:30:36 +04:00
belongs_to :priority , :class_name = > 'IssuePriority' , :foreign_key = > 'priority_id'
2007-03-12 20:59:02 +03:00
belongs_to :category , :class_name = > 'IssueCategory' , :foreign_key = > 'category_id'
has_many :journals , :as = > :journalized , :dependent = > :destroy
2012-10-04 01:36:19 +04:00
has_many :visible_journals ,
:class_name = > 'Journal' ,
:as = > :journalized ,
:conditions = > Proc . new {
[ " ( #{ Journal . table_name } .private_notes = ? OR ( #{ Project . allowed_to_condition ( User . current , :view_private_notes ) } )) " , false ]
} ,
:readonly = > true
2008-03-01 01:54:07 +03:00
has_many :time_entries , :dependent = > :delete_all
2008-06-29 12:55:37 +04:00
has_and_belongs_to_many :changesets , :order = > " #{ Changeset . table_name } .committed_on ASC, #{ Changeset . table_name } .id ASC "
2011-05-17 06:14:06 +04:00
2007-05-05 17:22:27 +04: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 06:14:06 +04:00
2011-01-22 14:46:15 +03:00
acts_as_nested_set :scope = > 'root_id' , :dependent = > :destroy
2011-07-24 13:34:23 +04:00
acts_as_attachable :after_add = > :attachment_added , :after_remove = > :attachment_removed
2008-06-28 00:13:56 +04:00
acts_as_customizable
2007-04-21 16:08:31 +04:00
acts_as_watchable
2008-07-20 21:26:07 +04:00
acts_as_searchable :columns = > [ 'subject' , " #{ table_name } .description " , " #{ Journal . table_name } .notes " ] ,
2012-10-04 01:36:19 +04:00
:include = > [ :project , :visible_journals ] ,
2008-07-20 21:26:07 +04:00
# sort by id so that limited eager loading doesn't break with postgresql
:order_column = > " #{ table_name } .id "
2009-08-02 08:36:06 +04:00
acts_as_event :title = > Proc . new { | o | " #{ o . tracker . name } # #{ o . id } ( #{ o . status } ): #{ o . subject } " } ,
2009-01-11 14:01:35 +03:00
:url = > Proc . new { | o | { :controller = > 'issues' , :action = > 'show' , :id = > o . id } } ,
:type = > Proc . new { | o | 'issue' + ( o . closed? ? ' closed' : '' ) }
2011-05-17 06:14:06 +04:00
2008-11-30 14:18:22 +03:00
acts_as_activity_provider :find_options = > { :include = > [ :project , :author , :tracker ] } ,
:author_key = > :author_id
2009-12-11 21:48:34 +03:00
DONE_RATIO_OPTIONS = %w( issue_field issue_status )
2010-03-04 19:18:51 +03:00
attr_reader :current_journal
2012-10-04 01:36:19 +04:00
delegate :notes , :notes = , :private_notes , :private_notes = , :to = > :current_journal , :allow_nil = > true
2010-03-10 08:10:43 +03:00
2009-01-07 22:47:24 +03:00
validates_presence_of :subject , :priority , :project , :tracker , :author , :status
2010-03-10 08:10:43 +03:00
2007-07-16 21:16:49 +04:00
validates_length_of :subject , :maximum = > 255
2007-03-12 20:59:02 +03:00
validates_inclusion_of :done_ratio , :in = > 0 .. 100
2013-01-05 20:34:41 +04:00
validates :estimated_hours , :numericality = > { :greater_than_or_equal_to = > 0 , :allow_nil = > true , :message = > :invalid }
2013-01-05 20:09:15 +04:00
validates :start_date , :date = > true
validates :due_date , :date = > true
2012-07-15 18:12:17 +04:00
validate :validate_issue , :validate_required_fields
2007-03-12 20:59:02 +03:00
2012-12-07 21:59:20 +04:00
scope :visible , lambda { | * args |
includes ( :project ) . where ( Issue . visible_condition ( args . shift || User . current , * args ) )
}
2011-05-17 06:14:06 +04:00
2012-04-27 03:51:10 +04:00
scope :open , lambda { | * args |
2011-12-18 17:05:46 +04:00
is_closed = args . size > 0 ? ! args . first : false
2012-12-07 21:59:20 +04:00
includes ( :status ) . where ( " #{ IssueStatus . table_name } .is_closed = ? " , is_closed )
2011-12-18 17:05:46 +04:00
}
2009-12-11 21:48:34 +03:00
2012-12-07 21:59:20 +04:00
scope :recently_updated , lambda { order ( " #{ Issue . table_name } .updated_on DESC " ) }
scope :on_active_project , lambda {
includes ( :status , :project , :tracker ) . where ( " #{ Project . table_name } .status = ? " , Project :: STATUS_ACTIVE )
}
2013-01-06 18:03:49 +04:00
scope :fixed_version , lambda { | versions |
ids = [ versions ] . flatten . compact . map { | v | v . is_a? ( Version ) ? v . id : v }
ids . any? ? where ( :fixed_version_id = > ids ) : where ( '1=0' )
}
2010-09-10 07:09:02 +04:00
2010-02-28 12:21:12 +03:00
before_create :default_assign
2012-05-21 22:23:03 +04:00
before_save :close_duplicates , :update_done_ratio_from_issue_status , :force_updated_on_change
2012-01-06 23:50:02 +04:00
after_save { | issue | issue . send :after_project_change if ! issue . id_changed? && issue . project_id_changed? }
2010-10-19 23:16:50 +04:00
after_save :reschedule_following_issues , :update_nested_set_attributes , :update_parent_attributes , :create_journal
2012-09-08 09:34:07 +04:00
# Should be after_create but would be called before previous after_save callbacks
after_save :after_create_from_copy
2010-03-13 17:56:49 +03:00
after_destroy :update_parent_attributes
2011-05-17 06:14:06 +04:00
2011-03-15 19:00:39 +03: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 21:53:15 +04:00
Project . allowed_to_condition ( user , :view_issues , options ) do | role , user |
2012-09-20 23:26:58 +04:00
if user . logged?
case role . issues_visibility
when 'all'
nil
when 'default'
2012-09-20 01:48:33 +04:00
user_ids = [ user . id ] + user . groups . map ( & :id )
" ( #{ 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 23:26:58 +04:00
when 'own'
2012-09-20 01:48:33 +04:00
user_ids = [ user . id ] + user . groups . map ( & :id )
" ( #{ table_name } .author_id = #{ user . id } OR #{ table_name } .assigned_to_id IN ( #{ user_ids . join ( ',' ) } )) "
else
'1=0'
end
2011-04-11 21:53:15 +04:00
else
2012-09-20 23:26:58 +04:00
" ( #{ table_name } .is_private = #{ connection . quoted_false } ) "
2011-04-11 21:53:15 +04:00
end
end
2011-03-15 19:00:39 +03:00
end
2009-01-31 16:22:29 +03:00
# Returns true if usr or current user is allowed to view the issue
def visible? ( usr = nil )
2011-04-11 21:53:15 +04:00
( usr || User . current ) . allowed_to? ( :view_issues , self . project ) do | role , user |
2012-09-20 23:26:58 +04:00
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
2011-04-11 21:53:15 +04:00
else
2012-09-20 23:26:58 +04:00
! self . is_private?
2011-04-11 21:53:15 +04:00
end
end
2009-01-31 16:22:29 +03:00
end
2011-05-17 06:14:06 +04:00
2012-12-22 14:30:42 +04:00
# Returns true if user or current user is allowed to edit or add a note to the issue
def editable? ( user = User . current )
user . allowed_to? ( :edit_issues , project ) || user . allowed_to? ( :add_issue_notes , project )
end
2011-12-18 18:57:58 +04:00
def initialize ( attributes = nil , * args )
super
2007-10-05 21:44:15 +04:00
if new_record?
# set default values for new records only
self . status || = IssueStatus . default
2009-05-31 03:30:36 +04:00
self . priority || = IssuePriority . default
2012-01-08 16:26:57 +04:00
self . watcher_user_ids = [ ]
2007-10-05 21:44:15 +04:00
end
end
2011-05-17 06:14:06 +04:00
2012-06-18 21:16:13 +04: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
rescue ActiveRecord :: RecordNotFound
# 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
2012-07-15 18:12:17 +04:00
def reload ( * args )
@workflow_rule_by_attribute = nil
2012-07-27 23:36:53 +04:00
@assignable_versions = nil
2012-07-15 18:12:17 +04:00
super
end
2008-06-28 00:13:56 +04:00
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
def available_custom_fields
2011-03-29 00:56:28 +04:00
( project && tracker ) ? ( project . all_issue_custom_fields & tracker . custom_fields . all ) : [ ]
2008-06-28 00:13:56 +04:00
end
2011-05-17 06:14:06 +04:00
2012-01-08 15:16:54 +04:00
# Copies attributes from another issue, arg can be an id or an Issue
2012-01-20 22:22:43 +04:00
def copy_from ( arg , options = { } )
2010-04-30 03:26:52 +04:00
issue = arg . is_a? ( Issue ) ? arg : Issue . visible . find ( arg )
2010-03-13 17:56:49 +03:00
self . attributes = issue . attributes . dup . except ( " id " , " root_id " , " parent_id " , " lft " , " rgt " , " created_on " , " updated_on " )
self . custom_field_values = issue . custom_field_values . inject ( { } ) { | h , v | h [ v . custom_field_id ] = v . value ; h }
2009-11-16 23:06:37 +03:00
self . status = issue . status
2012-01-07 00:06:25 +04:00
self . author = User . current
2012-01-20 22:22:43 +04:00
unless options [ :attachments ] == false
self . attachments = issue . attachments . map do | attachement |
attachement . copy ( :container = > self )
end
2012-01-20 21:56:28 +04:00
end
2012-01-08 15:16:54 +04:00
@copied_from = issue
2012-09-08 09:34:07 +04:00
@copy_options = options
2007-10-28 17:31:59 +03:00
self
end
2011-05-17 06:14:06 +04:00
2012-01-07 22:02:02 +04:00
# Returns an unsaved copy of the issue
2012-04-14 10:21:03 +04:00
def copy ( attributes = nil , copy_options = { } )
copy = self . class . new . copy_from ( self , copy_options )
2012-01-07 22:02:02 +04:00
copy . attributes = attributes if attributes
copy
end
2012-01-08 15:16:54 +04:00
# Returns true if the issue is a copy
def copy?
@copied_from . present?
end
2009-01-25 16:12:56 +03:00
# Moves/copies an issue to a new project and tracker
# Returns the moved/copied issue on success, false on failure
2012-01-07 00:46:45 +04:00
def move_to_project ( new_project , new_tracker = nil , options = { } )
2012-01-07 22:02:02 +04:00
ActiveSupport :: Deprecation . warn " Issue # move_to_project is deprecated, use # project= instead. "
2011-12-15 00:31:34 +04:00
if options [ :copy ]
2012-01-07 22:02:02 +04:00
issue = self . copy
2011-12-15 00:31:34 +04:00
else
issue = self
end
2011-05-17 06:14:06 +04:00
2012-01-07 00:34:42 +04:00
issue . init_journal ( User . current , options [ :notes ] )
2012-01-07 16:34:52 +04:00
# Preserve previous behaviour
# #move_to_project doesn't change tracker automatically
issue . send :project = , new_project , true
2010-03-13 17:56:49 +03:00
if new_tracker
issue . tracker = new_tracker
end
# Allow bulk setting of attributes on the issue
if options [ :attributes ]
issue . attributes = options [ :attributes ]
end
2012-01-07 00:34:42 +04:00
2012-01-07 00:46:45 +04:00
issue . save ? issue : false
2007-11-17 20:45:21 +03:00
end
2010-03-20 20:37:04 +03:00
def status_id = ( sid )
self . status = nil
2012-07-15 18:12:17 +04:00
result = write_attribute ( :status_id , sid )
@workflow_rule_by_attribute = nil
result
2010-03-20 20:37:04 +03:00
end
2011-05-17 06:14:06 +04:00
2007-10-05 21:44:15 +04:00
def priority_id = ( pid )
self . priority = nil
write_attribute ( :priority_id , pid )
2007-03-12 20:59:02 +03:00
end
2009-11-29 22:46:40 +03:00
2012-01-07 16:34:52 +04: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
2009-11-29 22:46:40 +03:00
def tracker_id = ( tid )
self . tracker = nil
2009-12-09 12:12:29 +03:00
result = write_attribute ( :tracker_id , tid )
@custom_field_values = nil
2012-07-15 18:12:17 +04:00
@workflow_rule_by_attribute = nil
2009-12-09 12:12:29 +03:00
result
2009-11-29 22:46:40 +03:00
end
2011-08-21 12:46:10 +04:00
2012-01-06 23:50:02 +04: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
end
2012-01-07 16:34:52 +04:00
def project = ( project , keep_tracker = false )
2012-01-06 23:50:02 +04:00
project_was = self . project
write_attribute ( :project_id , project ? project . id : nil )
association_instance_set ( 'project' , project )
if project_was && project && project_was != project
2012-07-27 23:36:53 +04:00
@assignable_versions = nil
2012-01-07 16:34:52 +04:00
unless keep_tracker || project . trackers . include? ( tracker )
self . tracker = project . trackers . first
end
2012-01-06 23:50:02 +04:00
# Reassign to the category with same name if any
if category
self . category = project . issue_categories . find_by_name ( category . name )
end
# 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 21:38:17 +04:00
# Clear the parent task if it's no longer valid
unless valid_parent_project?
2012-01-06 23:50:02 +04:00
self . parent_issue_id = nil
end
@custom_field_values = nil
end
end
2011-07-02 15:42:46 +04: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 06:14:06 +04:00
2012-04-25 21:17:49 +04:00
# Overrides assign_attributes so that project and tracker get assigned first
def assign_attributes_with_project_and_tracker_first ( new_attributes , * args )
2009-12-09 12:12:29 +03:00
return if new_attributes . nil?
2011-12-09 00:37:12 +04: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 12:12:29 +03:00
end
2012-04-25 21:17:49 +04:00
send :assign_attributes_without_project_and_tracker_first , attrs , * args
2009-12-09 12:12:29 +03:00
end
2010-02-14 15:43:48 +03:00
# Do not redefine alias chain on reload (see #4838)
2012-04-25 21:17:49 +04:00
alias_method_chain ( :assign_attributes , :project_and_tracker_first ) unless method_defined? ( :assign_attributes_without_project_and_tracker_first )
2011-05-17 06:14:06 +04:00
2008-04-26 15:59:51 +04:00
def estimated_hours = ( h )
write_attribute :estimated_hours , ( h . is_a? ( String ) ? h . to_hours : h )
end
2011-05-17 06:14:06 +04:00
2012-01-07 16:34:52 +04:00
safe_attributes 'project_id' ,
:if = > lambda { | issue , user |
2012-01-08 15:16:54 +04:00
if issue . new_record?
issue . copy?
elsif user . allowed_to? ( :move_issues , issue . project )
2012-01-07 16:39:26 +04:00
projects = Issue . allowed_target_projects_on_move ( user )
projects . include? ( issue . project ) && projects . size > 1
end
2012-01-07 16:34:52 +04:00
}
2010-12-12 16:11:53 +03:00
safe_attributes 'tracker_id' ,
'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-04 01:36:19 +04:00
'notes' ,
2010-12-12 16:11:53 +03:00
:if = > lambda { | issue , user | issue . new_record? || user . allowed_to? ( :edit_issues , issue . project ) }
2011-05-17 06:14:06 +04:00
2010-12-12 16:11:53 +03:00
safe_attributes 'status_id' ,
'assigned_to_id' ,
'fixed_version_id' ,
'done_ratio' ,
2011-12-15 14:41:49 +04:00
'lock_version' ,
2012-10-04 01:36:19 +04:00
'notes' ,
2010-12-12 16:11:53 +03:00
:if = > lambda { | issue , user | issue . new_statuses_allowed_to ( user ) . any? }
2010-11-11 19:37:16 +03:00
2012-10-04 01:36:19 +04:00
safe_attributes 'notes' ,
:if = > lambda { | issue , user | user . allowed_to? ( :add_issue_notes , issue . project ) }
safe_attributes 'private_notes' ,
:if = > lambda { | issue , user | ! issue . new_record? && user . allowed_to? ( :set_notes_private , issue . project ) }
2011-12-13 23:50:44 +04:00
safe_attributes 'watcher_user_ids' ,
:if = > lambda { | issue , user | issue . new_record? && user . allowed_to? ( :add_issue_watchers , issue . project ) }
2011-04-15 17:23:13 +04:00
safe_attributes 'is_private' ,
:if = > lambda { | issue , user |
user . allowed_to? ( :set_issues_private , issue . project ) ||
( issue . author == user && user . allowed_to? ( :set_own_issues_private , issue . project ) )
}
2011-05-17 06:14:06 +04:00
2011-12-13 23:56:33 +04:00
safe_attributes 'parent_issue_id' ,
:if = > lambda { | issue , user | ( issue . new_record? || user . allowed_to? ( :edit_issues , issue . project ) ) &&
user . allowed_to? ( :manage_subtasks , issue . project ) }
2012-07-15 18:12:17 +04:00
def safe_attribute_names ( user = nil )
names = super
2012-07-05 16:20:07 +04:00
names -= disabled_core_fields
2012-07-15 18:12:17 +04:00
names -= read_only_attribute_names ( user )
2012-07-05 16:20:07 +04:00
names
end
2010-01-12 23:17:20 +03: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 )
2010-11-11 19:37:16 +03:00
return unless attrs . is_a? ( Hash )
2011-05-17 06:14:06 +04:00
2012-07-05 16:20:07 +04:00
attrs = attrs . dup
2011-05-17 06:14:06 +04:00
2012-01-07 16:34:52 +04:00
# Project and Tracker must be set before since new_statuses_allowed_to depends on it.
2012-07-05 16:20:07 +04:00
if ( p = attrs . delete ( 'project_id' ) ) && safe_attribute? ( 'project_id' )
2012-01-08 15:48:36 +04:00
if allowed_target_projects ( user ) . collect ( & :id ) . include? ( p . to_i )
self . project_id = p
end
2012-01-07 16:34:52 +04:00
end
2012-01-08 15:48:36 +04:00
2012-07-05 16:20:07 +04:00
if ( t = attrs . delete ( 'tracker_id' ) ) && safe_attribute? ( 'tracker_id' )
2010-11-12 14:34:53 +03:00
self . tracker_id = t
end
2011-05-17 06:14:06 +04:00
2012-07-15 18:12:17 +04:00
if ( s = attrs . delete ( 'status_id' ) ) && safe_attribute? ( 'status_id' )
if new_statuses_allowed_to ( user ) . collect ( & :id ) . include? ( s . to_i )
self . status_id = s
2010-02-24 00:26:29 +03:00
end
end
2011-05-17 06:14:06 +04:00
2012-07-15 18:12:17 +04:00
attrs = delete_unsafe_attributes ( attrs , user )
return if attrs . empty?
2010-03-13 17:56:49 +03:00
unless leaf?
attrs . reject! { | k , v | %w( priority_id done_ratio start_date due_date estimated_hours ) . include? ( k ) }
end
2011-05-17 06:14:06 +04:00
2011-12-13 23:56:33 +04:00
if attrs [ 'parent_issue_id' ] . present?
2012-10-14 18:35:57 +04:00
s = attrs [ 'parent_issue_id' ] . to_s
unless ( m = s . match ( %r{ \ A # ?( \ d+) \ z } ) ) && Issue . visible ( user ) . exists? ( m [ 1 ] )
2012-10-12 17:40:41 +04:00
@invalid_parent_issue_id = attrs . delete ( 'parent_issue_id' )
end
2010-03-13 17:56:49 +03:00
end
2011-05-17 06:14:06 +04:00
2012-07-15 18:12:17 +04:00
if attrs [ 'custom_field_values' ] . present?
attrs [ 'custom_field_values' ] = attrs [ 'custom_field_values' ] . reject { | k , v | read_only_attribute_names ( user ) . include? k . to_s }
end
if attrs [ 'custom_fields' ] . present?
attrs [ 'custom_fields' ] = attrs [ 'custom_fields' ] . reject { | c | read_only_attribute_names ( user ) . include? c [ 'id' ] . to_s }
end
2011-12-13 23:50:44 +04:00
# mass-assignment security bypass
2012-04-25 21:17:49 +04:00
assign_attributes attrs , :without_protection = > true
2010-01-12 23:17:20 +03:00
end
2011-05-17 06:14:06 +04:00
2012-07-05 16:20:07 +04:00
def disabled_core_fields
tracker ? tracker . disabled_core_fields : [ ]
end
2012-07-15 18:12:17 +04:00
# Returns the custom_field_values that can be edited by the given user
def editable_custom_field_values ( user = nil )
custom_field_values . reject do | value |
read_only_attribute_names ( user ) . include? ( value . custom_field_id . to_s )
end
end
# 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 18:50:38 +04:00
workflow_rule_by_attribute ( user ) . reject { | attr , rule | rule != 'readonly' } . keys
2012-07-15 18:12:17 +04: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 18:50:38 +04:00
workflow_rule_by_attribute ( user ) . reject { | attr , rule | rule != 'required' } . keys
2012-07-15 18:12:17 +04: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
roles = user_real . admin ? Role . all : user_real . roles_for_project ( project )
return { } if roles . empty?
result = { }
workflow_permissions = WorkflowPermission . where ( :tracker_id = > tracker_id , :old_status_id = > status_id , :role_id = > roles . map ( & :id ) ) . all
if workflow_permissions . any?
workflow_rules = workflow_permissions . inject ( { } ) do | h , wp |
h [ wp . field_name ] || = [ ]
h [ wp . field_name ] << wp . rule
h
end
workflow_rules . each do | attr , rules |
next if rules . size < roles . size
uniq_rules = rules . uniq
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 21:48:34 +03:00
def done_ratio
2010-09-26 22:13:31 +04:00
if Issue . use_status_for_done_ratio? && status && status . default_done_ratio
2009-12-12 14:02:53 +03:00
status . default_done_ratio
2009-12-11 21:48:34 +03: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 06:14:06 +04:00
2011-09-21 10:12:35 +04:00
def validate_issue
2012-10-18 20:47:41 +04:00
if due_date && start_date && due_date < start_date
2009-02-21 14:04:50 +03:00
errors . add :due_date , :greater_than_start_date
2007-03-12 20:59:02 +03:00
end
2011-05-17 06:14:06 +04:00
2007-05-05 17:22:27 +04:00
if start_date && soonest_start && start_date < soonest_start
2009-02-21 14:04:50 +03:00
errors . add :start_date , :invalid
2007-05-05 17:22:27 +04:00
end
2011-05-17 06:14:06 +04:00
2009-11-08 16:03:41 +03:00
if fixed_version
if ! assignable_versions . include? ( fixed_version )
errors . add :fixed_version_id , :inclusion
elsif reopened? && fixed_version . closed?
2011-10-07 02:49:26 +04:00
errors . add :base , I18n . t ( :error_can_not_reopen_issue_on_closed_version )
2009-11-08 16:03:41 +03:00
end
end
2011-05-17 06:14:06 +04:00
2009-11-29 22:46:40 +03:00
# Checks that the issue can not be added/moved to a disabled tracker
if project && ( tracker_id_changed? || project_id_changed? )
unless project . trackers . include? ( tracker )
errors . add :tracker_id , :inclusion
end
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
# Checks parent issue assignment
2012-10-12 17:40:41 +04:00
if @invalid_parent_issue_id . present?
errors . add :parent_issue_id , :invalid
elsif @parent_issue
2012-10-10 21:38:17 +04:00
if ! valid_parent_project? ( @parent_issue )
errors . add :parent_issue_id , :invalid
2010-03-13 17:56:49 +03:00
elsif ! new_record?
# moving an existing issue
if @parent_issue . root_id != root_id
# we can always move to another tree
elsif move_possible? ( @parent_issue )
# move accepted inside tree
else
2012-10-10 21:38:17 +04:00
errors . add :parent_issue_id , :invalid
2010-03-13 17:56:49 +03:00
end
end
end
2007-03-12 20:59:02 +03:00
end
2011-05-17 06:14:06 +04:00
2012-07-15 18:12:17 +04: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 }
if v && v . value . blank?
errors . add :base , v . custom_field . name + ' ' + l ( 'activerecord.errors.messages.blank' )
end
else
if respond_to? ( attribute ) && send ( attribute ) . blank?
errors . add attribute , :blank
end
end
end
end
2009-12-11 21:48:34 +03: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 22:13:31 +04:00
if Issue . use_status_for_done_ratio? && status && status . default_done_ratio
2009-12-12 13:33:12 +03:00
self . done_ratio = status . default_done_ratio
2009-12-11 21:48:34 +03:00
end
end
2011-05-17 06:14:06 +04:00
2007-03-12 20:59:02 +03:00
def init_journal ( user , notes = " " )
@current_journal || = Journal . new ( :journalized = > self , :user = > user , :notes = > notes )
2012-01-07 00:34:42 +04:00
if new_record?
@current_journal . notify = false
else
@attributes_before_change = attributes . dup
@custom_values_before_change = { }
2012-01-30 00:51:48 +04:00
self . custom_field_values . each { | c | @custom_values_before_change . store c . custom_field_id , c . value }
2012-01-07 00:34:42 +04:00
end
2007-03-12 20:59:02 +03:00
@current_journal
end
2011-05-17 06:14:06 +04:00
2012-02-04 21:36:15 +04:00
# Returns the id of the last journal or nil
def last_journal_id
if new_record?
nil
else
2012-08-08 22:47:52 +04:00
journals . maximum ( :id )
2012-02-04 21:36:15 +04:00
end
end
2012-08-09 01:28:07 +04: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
2007-08-25 21:45:51 +04:00
# Return true if the issue is closed, otherwise false
def closed?
self . status . is_closed?
end
2011-05-17 06:14:06 +04:00
2009-11-08 16:03:41 +03:00
# Return true if the issue is being reopened
def reopened?
if ! new_record? && status_id_changed?
status_was = IssueStatus . find_by_id ( status_id_was )
status_new = IssueStatus . find_by_id ( status_id )
if status_was && status_new && status_was . is_closed? && ! status_new . is_closed?
return true
end
end
false
end
2010-02-28 12:21:12 +03:00
# Return true if the issue is being closed
def closing?
if ! new_record? && status_id_changed?
status_was = IssueStatus . find_by_id ( status_id_was )
status_new = IssueStatus . find_by_id ( status_id )
if status_was && status_new && ! status_was . is_closed? && status_new . is_closed?
return true
end
end
false
end
2011-05-17 06:14:06 +04:00
2008-12-17 00:13:35 +03:00
# Returns true if the issue is overdue
def overdue?
2009-02-03 20:32:07 +03:00
! due_date . nil? && ( due_date < Date . today ) && ! status . is_closed?
2008-12-17 00:13:35 +03:00
end
2010-09-10 07:09:02 +04: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?
done_date = start_date + ( ( due_date - start_date + 1 ) * done_ratio / 100 ) . floor
return done_date < = Date . today
end
2010-09-20 06:55:26 +04:00
# Does this issue have children?
def children?
! leaf?
end
2011-05-17 06:14:06 +04:00
2007-08-16 21:47:41 +04:00
# Users the issue can be assigned to
def assignable_users
2010-10-07 21:28:29 +04:00
users = project . assignable_users
users << author if author
2011-07-23 23:24:02 +04:00
users << assigned_to if assigned_to
2010-10-07 21:46:37 +04:00
users . uniq . sort
2007-08-16 21:47:41 +04:00
end
2011-05-17 06:14:06 +04:00
2009-11-08 16:03:41 +03:00
# Versions that the issue can be assigned to
def assignable_versions
2012-07-27 23:36:53 +04:00
return @assignable_versions if @assignable_versions
versions = project . shared_versions . open . all
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 16:03:41 +03:00
end
2011-05-17 06:14:06 +04:00
2009-07-04 16:07:03 +04: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 06:14:06 +04:00
2012-04-09 13:39:27 +04:00
# Returns an array of statuses that user is able to apply
2012-02-09 23:51:38 +04:00
def new_statuses_allowed_to ( user = User . current , include_default = false )
2012-04-14 09:45:16 +04:00
if new_record? && @copied_from
[ IssueStatus . default , @copied_from . status ] . compact . uniq . sort
else
initial_status = nil
if new_record?
initial_status = IssueStatus . default
elsif status_id_was
initial_status = IssueStatus . find_by_id ( status_id_was )
end
initial_status || = status
statuses = initial_status . find_new_statuses_allowed_to (
user . admin ? Role . all : user . roles_for_project ( project ) ,
tracker ,
author == user ,
assigned_to_id_changed? ? assigned_to_id_was == user . id : assigned_to_id == user . id
)
statuses << initial_status unless statuses . empty?
statuses << IssueStatus . default if include_default
statuses = statuses . compact . uniq . sort
blocked? ? statuses . reject { | s | s . is_closed? } : statuses
end
2008-01-06 20:06:14 +03:00
end
2011-05-17 06:14:06 +04:00
2012-01-23 21:55:29 +04:00
def assigned_to_was
if assigned_to_id_changed? && assigned_to_id_was . present?
@assigned_to_was || = User . find_by_id ( assigned_to_id_was )
end
end
2012-10-04 01:36:19 +04:00
# Returns the users that should be notified
def notified_users
2012-01-23 21:55:29 +04:00
notified = [ ]
2010-09-28 22:22:10 +04:00
# Author and assignee are always notified unless they have been
# locked or don't want to be notified
2012-01-23 21:55:29 +04:00
notified << author if author
2011-07-23 22:18:13 +04:00
if assigned_to
2012-01-23 21:55:29 +04:00
notified += ( assigned_to . is_a? ( Group ) ? assigned_to . users : [ assigned_to ] )
end
if assigned_to_was
notified += ( assigned_to_was . is_a? ( Group ) ? assigned_to_was . users : [ assigned_to_was ] )
2011-07-23 22:18:13 +04:00
end
2012-01-23 21:55:29 +04:00
notified = notified . select { | u | u . active? && u . notify_about? ( self ) }
notified += project . notified_users
2009-12-04 00:28:14 +03:00
notified . uniq!
# Remove users that can not view the issue
notified . reject! { | user | ! visible? ( user ) }
2012-10-04 01:36:19 +04:00
notified
end
# Returns the email addresses that should be notified
def recipients
notified_users . collect ( & :mail )
2009-12-04 00:28:14 +03:00
end
2011-05-17 06:14:06 +04:00
2011-12-04 20:43:32 +04:00
# Returns the number of hours spent on this issue
def spent_hours
@spent_hours || = time_entries . sum ( :hours ) || 0
end
2010-03-13 17:56:49 +03:00
# Returns the total number of hours spent on this issue and its descendants
2009-03-21 03:39:53 +03:00
#
# Example:
2010-03-13 17:56:49 +03:00
# spent_hours => 0.0
# spent_hours => 50.2
2011-12-04 20:43:32 +04:00
def total_spent_hours
2011-12-18 12:21:29 +04:00
@total_spent_hours || = self_and_descendants . sum ( " #{ TimeEntry . table_name } .hours " ,
:joins = > " LEFT JOIN #{ TimeEntry . table_name } ON #{ TimeEntry . table_name } .issue_id = #{ Issue . table_name } .id " ) . to_f || 0 . 0
2007-03-23 15:22:31 +03:00
end
2011-05-17 06:14:06 +04:00
2007-05-05 17:22:27 +04:00
def relations
2012-12-11 01:59:01 +04:00
@relations || = IssueRelation :: Relations . new ( self , ( relations_from + relations_to ) . sort )
2011-07-24 19:34:41 +04:00
end
2011-08-21 12:46:10 +04:00
2011-07-24 19:34:41 +04:00
# Preloads relations for a collection of issues
def self . load_relations ( issues )
if issues . any?
relations = IssueRelation . all ( :conditions = > [ " issue_from_id IN (:ids) OR issue_to_id IN (:ids) " , { :ids = > issues . map ( & :id ) } ] )
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 17:22:27 +04:00
end
2011-08-21 12:46:10 +04:00
2011-12-04 20:43:32 +04:00
# Preloads visible spent time for a collection of issues
def self . load_visible_spent_hours ( issues , user = User . current )
if issues . any?
hours_by_issue_id = TimeEntry . visible ( user ) . sum ( :hours , :group = > :issue_id )
issues . each do | issue |
issue . instance_variable_set " @spent_hours " , ( hours_by_issue_id [ issue . id ] || 0 )
end
end
end
2012-09-29 16:57:38 +04: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
relations_from = IssueRelation . includes ( :issue_to = > [ :status , :project ] ) . where ( visible_condition ( user ) ) . where ( :issue_from_id = > issue_ids ) . all
# Relations with issue_to in given issues and visible issue_from
relations_to = IssueRelation . includes ( :issue_from = > [ :status , :project ] ) . where ( visible_condition ( user ) ) . where ( :issue_to_id = > issue_ids ) . all
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-11 01:59:01 +04:00
issue . instance_variable_set " @relations " , IssueRelation :: Relations . new ( issue , relations . sort )
2012-09-29 16:57:38 +04:00
end
end
end
2011-07-04 21:03:04 +04:00
# Finds an issue relation given its id.
def find_relation ( relation_id )
IssueRelation . find ( relation_id , :conditions = > [ " issue_to_id = ? OR issue_from_id = ? " , id , id ] )
end
2011-05-17 06:14:06 +04:00
2011-03-13 16:00:55 +03:00
def all_dependent_issues ( except = [ ] )
except << self
2007-05-05 17:22:27 +04:00
dependencies = [ ]
relations_from . each do | relation |
2011-05-17 06:14:06 +04:00
if relation . issue_to && ! except . include? ( relation . issue_to )
2011-01-15 17:12:18 +03:00
dependencies << relation . issue_to
dependencies += relation . issue_to . all_dependent_issues ( except )
end
2007-05-05 17:22:27 +04:00
end
dependencies
end
2011-05-17 06:14:06 +04:00
2008-06-03 22:30:29 +04:00
# Returns an array of issues that duplicate this one
2007-08-25 21:45:51 +04:00
def duplicates
2008-06-03 22:30:29 +04:00
relations_to . select { | r | r . relation_type == IssueRelation :: TYPE_DUPLICATES } . collect { | r | r . issue_from }
2007-08-25 21:45:51 +04:00
end
2011-05-17 06:14:06 +04:00
2008-05-25 17:26:21 +04: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 06:14:06 +04:00
2009-03-21 03:39:53 +03:00
# Returns the time scheduled for this issue.
2011-05-17 06:14:06 +04:00
#
2009-03-21 03:39:53 +03:00
# Example:
# Start Date: 2/26/09, End Date: 3/04/09
# duration => 6
2007-05-05 17:22:27 +04:00
def duration
( start_date && due_date ) ? due_date - start_date : 0
end
2011-05-17 06:14:06 +04:00
2012-10-29 14:06:30 +04: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 17:43:52 +04:00
def soonest_start ( reload = false )
@soonest_start = nil if reload
2010-03-13 18:29:34 +03:00
@soonest_start || = (
2012-11-24 17:43:52 +04:00
relations_to ( reload ) . collect { | relation | relation . successor_soonest_start } +
2010-03-13 18:29:34 +03:00
ancestors . collect ( & :soonest_start )
) . compact . max
end
2011-05-17 06:14:06 +04:00
2012-10-29 14:06:30 +04: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 18:29:34 +03:00
return if date . nil?
if leaf?
2012-11-24 17:43:52 +04: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 14:06:30 +04:00
reschedule_on ( date )
2012-02-12 23:41:42 +04:00
begin
save
rescue ActiveRecord :: StaleObjectError
reload
2012-10-29 14:06:30 +04:00
reschedule_on ( date )
2012-02-12 23:41:42 +04:00
save
end
2010-03-13 18:29:34 +03:00
end
else
leaves . each do | leaf |
2012-11-24 19:09:51 +04: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
if start_date == leaf . start_date || date > leaf . start_date
leaf . reschedule_on! ( date )
end
else
leaf . reschedule_on! ( date )
end
2010-03-13 18:29:34 +03:00
end
end
2007-05-05 17:22:27 +04:00
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03: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 06:14:06 +04:00
2008-04-02 02:42:10 +04:00
def to_s
" #{ tracker } # #{ id } : #{ subject } "
end
2011-05-17 06:14:06 +04:00
2009-04-25 15:28:48 +04:00
# Returns a string of css classes that apply to the issue
def css_classes
2012-10-26 05:25:23 +04:00
s = " issue status- #{ status_id } #{ priority . try ( :css_classes ) } "
2009-04-25 15:28:48 +04:00
s << ' closed' if closed?
s << ' overdue' if overdue?
2011-03-25 21:31:32 +03:00
s << ' child' if child?
s << ' parent' unless leaf?
2011-04-15 17:23:13 +04:00
s << ' private' if is_private?
2009-04-25 15:28:48 +04:00
s << ' created-by-me' if User . current . logged? && author_id == User . current . id
s << ' assigned-to-me' if User . current . logged? && assigned_to_id == User . current . id
s
end
2009-12-06 13:28:20 +03:00
2012-02-17 01:00:11 +04:00
# Saves an issue and a time_entry from the parameters
2010-03-05 20:11:50 +03:00
def save_issue_with_child_records ( params , existing_time_entry = nil )
2010-04-11 20:48:46 +04:00
Issue . transaction do
2011-03-13 19:38:01 +03:00
if params [ :time_entry ] && ( params [ :time_entry ] [ :hours ] . present? || params [ :time_entry ] [ :comments ] . present? ) && User . current . allowed_to? ( :log_time , project )
2010-04-11 20:48:46 +04:00
@time_entry = existing_time_entry || TimeEntry . new
@time_entry . project = project
@time_entry . issue = self
@time_entry . user = User . current
2011-11-25 00:39:24 +04:00
@time_entry . spent_on = User . current . today
2010-04-11 20:48:46 +04:00
@time_entry . attributes = params [ :time_entry ]
self . time_entries << @time_entry
end
2011-05-17 06:14:06 +04:00
2012-02-17 01:00:11 +04:00
# TODO: Rename hook
Redmine :: Hook . call_hook ( :controller_issues_edit_before_save , { :params = > params , :issue = > self , :time_entry = > @time_entry , :journal = > @current_journal } )
if save
2010-04-11 20:48:46 +04:00
# TODO: Rename hook
2012-02-17 01:00:11 +04:00
Redmine :: Hook . call_hook ( :controller_issues_edit_after_save , { :params = > params , :issue = > self , :time_entry = > @time_entry , :journal = > @current_journal } )
else
raise ActiveRecord :: Rollback
2010-03-05 20:11:50 +03:00
end
end
end
2009-12-08 23:47:52 +03: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 06:14:06 +04:00
2009-12-08 23:47:52 +03: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
Issue . update_versions ( [ " #{ Version . table_name } .project_id IN (?) OR #{ Issue . table_name } .project_id IN (?) " , moved_project_ids , moved_project_ids ] )
end
2010-03-13 17:56:49 +03:00
def parent_issue_id = ( arg )
2012-10-12 17:40:41 +04:00
s = arg . to_s . strip . presence
if s && ( m = s . match ( %r{ \ A # ?( \ d+) \ z } ) ) && ( @parent_issue = Issue . find_by_id ( m [ 1 ] ) )
2010-03-13 17:56:49 +03:00
@parent_issue . id
else
@parent_issue = nil
2012-10-12 17:40:41 +04:00
@invalid_parent_issue_id = arg
2010-03-13 17:56:49 +03:00
end
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
def parent_issue_id
2012-10-12 17:40:41 +04:00
if @invalid_parent_issue_id
@invalid_parent_issue_id
elsif instance_variable_defined? :@parent_issue
2010-03-13 17:56:49 +03:00
@parent_issue . nil? ? nil : @parent_issue . id
else
parent_id
end
end
2012-10-10 21:38:17 +04:00
# Returns true if issue's project is a valid
# parent issue project
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
2010-02-03 19:49:21 +03:00
# Extracted from the ReportsController.
def self . by_tracker ( project )
2010-02-04 20:24:33 +03:00
count_and_group_by ( :project = > project ,
:field = > 'tracker_id' ,
:joins = > Tracker . table_name )
2010-02-03 19:49:21 +03:00
end
def self . by_version ( project )
2010-02-04 20:24:33 +03:00
count_and_group_by ( :project = > project ,
:field = > 'fixed_version_id' ,
:joins = > Version . table_name )
2010-02-03 19:49:21 +03:00
end
def self . by_priority ( project )
2010-02-04 20:24:33 +03:00
count_and_group_by ( :project = > project ,
:field = > 'priority_id' ,
:joins = > IssuePriority . table_name )
2010-02-03 19:49:21 +03:00
end
def self . by_category ( project )
2010-02-04 20:24:33 +03:00
count_and_group_by ( :project = > project ,
:field = > 'category_id' ,
:joins = > IssueCategory . table_name )
2010-02-03 19:49:21 +03:00
end
def self . by_assigned_to ( project )
2010-02-04 20:24:33 +03:00
count_and_group_by ( :project = > project ,
:field = > 'assigned_to_id' ,
:joins = > User . table_name )
2010-02-03 19:49:21 +03:00
end
def self . by_author ( project )
2010-02-04 20:24:33 +03:00
count_and_group_by ( :project = > project ,
:field = > 'author_id' ,
:joins = > User . table_name )
2010-02-03 19:49:21 +03:00
end
def self . by_subproject ( project )
ActiveRecord :: Base . connection . select_all ( " select s.id as status_id,
s . is_closed as closed ,
2011-04-08 16:21:06 +04:00
#{Issue.table_name}.project_id as project_id,
count ( #{Issue.table_name}.id) as total
2010-02-03 19:49:21 +03:00
from
2011-04-08 16:21:06 +04:00
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
2010-02-03 19:49:21 +03:00
where
2011-04-08 16:21:06 +04:00
#{Issue.table_name}.status_id=s.id
and #{Issue.table_name}.project_id = #{Project.table_name}.id
and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
and #{Issue.table_name}.project_id <> #{project.id}
group by s . id , s . is_closed , #{Issue.table_name}.project_id") if project.descendants.active.any?
2010-02-03 19:49:21 +03:00
end
# End ReportsController extraction
2011-05-17 06:14:06 +04:00
2012-01-08 15:48:36 +04:00
# Returns an array of projects that user can assign the issue to
def allowed_target_projects ( user = User . current )
if new_record?
Project . all ( :conditions = > Project . allowed_to_condition ( user , :add_issues ) )
else
self . class . allowed_target_projects_on_move ( user )
end
end
# Returns an array of projects that user can move issues to
2012-01-07 16:34:52 +04:00
def self . allowed_target_projects_on_move ( user = User . current )
2012-03-25 23:32:05 +04:00
Project . all ( :conditions = > Project . allowed_to_condition ( user , :move_issues ) )
2010-04-18 16:47:41 +04:00
end
2011-05-17 06:14:06 +04:00
2009-12-08 23:47:52 +03:00
private
2011-05-17 06:14:06 +04:00
2012-01-06 23:50:02 +04:00
def after_project_change
# Update project_id on related time entries
TimeEntry . update_all ( [ " project_id = ? " , project_id ] , { :issue_id = > id } )
# Delete issue relations
unless Setting . cross_project_issue_relations?
relations_from . clear
relations_to . clear
end
2012-10-10 21:38:17 +04:00
# Move subtasks that were in the same project
2012-01-06 23:50:02 +04:00
children . each do | child |
2012-10-10 21:38:17 +04:00
next unless child . project_id == project_id_was
2012-01-07 16:34:52 +04:00
# Change project and keep project
child . send :project = , project , true
2012-01-06 23:50:02 +04:00
unless child . save
raise ActiveRecord :: Rollback
end
end
end
2012-09-27 23:09:30 +04: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 09:34:07 +04:00
def after_create_from_copy
2012-09-27 23:09:30 +04:00
return unless copy? && ! @after_create_from_copy_handled
2012-09-08 09:34:07 +04:00
2012-09-28 02:27:37 +04:00
if ( @copied_from . project_id == project_id || Setting . cross_project_issue_relations? ) && @copy_options [ :link ] != false
2012-09-27 23:09:30 +04: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
2012-09-08 09:34:07 +04:00
@copied_from . children . each do | child |
unless child . visible?
# Do not copy subtasks that are not visible to avoid potential disclosure of private data
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
copy = Issue . new . copy_from ( child , @copy_options )
copy . author = author
copy . project = project
copy . parent_issue_id = id
# Children subtasks are copied recursively
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
end
end
end
2012-09-27 23:09:30 +04:00
@after_create_from_copy_handled = true
2012-09-08 09:34:07 +04:00
end
2010-03-13 17:56:49 +03:00
def update_nested_set_attributes
if root_id . nil?
# issue was just created
self . root_id = ( @parent_issue . nil? ? id : @parent_issue . root_id )
set_default_left_and_right
Issue . update_all ( " root_id = #{ root_id } , lft = #{ lft } , rgt = #{ rgt } " , [ " id = ? " , id ] )
if @parent_issue
move_to_child_of ( @parent_issue )
end
reload
elsif parent_issue_id != parent_id
2010-06-30 06:45:34 +04:00
former_parent_id = parent_id
2010-03-13 17:56:49 +03:00
# moving an existing issue
if @parent_issue && @parent_issue . root_id == root_id
# inside the same tree
move_to_child_of ( @parent_issue )
else
# to another tree
unless root?
move_to_right_of ( root )
reload
end
old_root_id = root_id
self . root_id = ( @parent_issue . nil? ? id : @parent_issue . root_id )
target_maxright = nested_set_scope . maximum ( right_column_name ) || 0
offset = target_maxright + 1 - lft
Issue . update_all ( " root_id = #{ root_id } , lft = lft + #{ offset } , rgt = rgt + #{ offset } " ,
[ " root_id = ? AND lft >= ? AND rgt <= ? " , old_root_id , lft , rgt ] )
self [ left_column_name ] = lft + offset
self [ right_column_name ] = rgt + offset
if @parent_issue
move_to_child_of ( @parent_issue )
end
end
reload
# delete invalid relations of all descendants
self_and_descendants . each do | issue |
issue . relations . each do | relation |
relation . destroy unless relation . valid?
end
end
2010-06-30 06:45:34 +04:00
# update former parent
recalculate_attributes_for ( former_parent_id ) if former_parent_id
2010-03-13 17:56:49 +03:00
end
remove_instance_variable ( :@parent_issue ) if instance_variable_defined? ( :@parent_issue )
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
def update_parent_attributes
2010-06-30 06:45:34 +04:00
recalculate_attributes_for ( parent_id ) if parent_id
end
def recalculate_attributes_for ( issue_id )
if issue_id && p = Issue . find_by_id ( issue_id )
2010-03-13 17:56:49 +03:00
# priority = highest priority of children
2011-12-26 16:07:07 +04:00
if priority_position = p . children . maximum ( " #{ IssuePriority . table_name } .position " , :joins = > :priority )
2010-03-13 17:56:49 +03:00
p . priority = IssuePriority . find_by_position ( priority_position )
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
# 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
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
# done ratio = weighted average ratio of leaves
2010-09-26 22:13:31 +04:00
unless Issue . use_status_for_done_ratio? && p . status && p . status . default_done_ratio
2010-03-13 17:56:49 +03:00
leaves_count = p . leaves . count
if leaves_count > 0
average = p . leaves . average ( :estimated_hours ) . to_f
if average == 0
average = 1
end
2011-12-26 16:07:07 +04:00
done = p . leaves . sum ( " COALESCE(estimated_hours, #{ average } ) * (CASE WHEN is_closed = #{ connection . quoted_true } THEN 100 ELSE COALESCE(done_ratio, 0) END) " , :joins = > :status ) . to_f
2010-03-13 17:56:49 +03:00
progress = done / ( average * leaves_count )
p . done_ratio = progress . round
end
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
# estimate = sum of leaves estimates
p . estimated_hours = p . leaves . sum ( :estimated_hours ) . to_f
p . estimated_hours = nil if p . estimated_hours == 0 . 0
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
# ancestors will be recursively updated
2012-04-25 21:17:49 +04:00
p . save ( :validate = > false )
2010-03-13 17:56:49 +03:00
end
end
2011-05-17 06:14:06 +04:00
2009-12-08 23:47:52 +03: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
2011-12-18 19:02:46 +04:00
Issue . scoped ( :conditions = > conditions ) . all (
:conditions = > " #{ 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' " ,
:include = > [ :project , :fixed_version ]
) . each do | issue |
2009-12-06 13:28:20 +03: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 12:46:10 +04:00
2012-09-13 01:30:30 +04:00
# Callback on file attachment
2011-07-24 13:34:23 +04:00
def attachment_added ( obj )
if @current_journal && ! obj . new_record?
@current_journal . details << JournalDetail . new ( :property = > 'attachment' , :prop_key = > obj . id , :value = > obj . filename )
end
end
2011-05-17 06:14:06 +04:00
2008-12-09 19:54:46 +03:00
# Callback on attachment deletion
def attachment_removed ( obj )
2012-03-04 15:41:10 +04:00
if @current_journal && ! obj . new_record?
@current_journal . details << JournalDetail . new ( :property = > 'attachment' , :prop_key = > obj . id , :old_value = > obj . filename )
@current_journal . save
end
2008-12-09 19:54:46 +03:00
end
2011-05-17 06:14:06 +04:00
2010-02-28 12:21:12 +03:00
# Default assignment based on category
def default_assign
if assigned_to . nil? && category && category . assigned_to
self . assigned_to = category . assigned_to
end
end
# Updates start/due dates of following issues
def reschedule_following_issues
if start_date_changed? || due_date_changed?
relations_from . each do | relation |
relation . set_issue_to_dates
end
end
end
# Closes duplicates if the issue is being closed
def close_duplicates
if closing?
duplicates . each do | duplicate |
# Reload is need in case the duplicate was updated by a previous duplicate
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 )
end
duplicate . update_attribute :status , self . status
end
end
end
2011-05-17 06:14:06 +04:00
2012-05-21 22:23:03 +04:00
# Make sure updated_on is updated when adding a note
def force_updated_on_change
if @current_journal
self . updated_on = current_time_from_proper_timezone
end
end
2009-04-08 23:11:30 +04:00
# Saves the changes in a Journal
# Called after_save
def create_journal
if @current_journal
# attributes changes
2012-01-07 00:34:42 +04:00
if @attributes_before_change
( Issue . column_names - %w( id root_id lft rgt lock_version created_on updated_on ) ) . each { | c |
before = @attributes_before_change [ c ]
after = send ( c )
next if before == after || ( before . blank? && after . blank? )
@current_journal . details << JournalDetail . new ( :property = > 'attr' ,
:prop_key = > c ,
:old_value = > before ,
:value = > after )
}
end
if @custom_values_before_change
# custom fields changes
2012-01-30 00:51:48 +04:00
custom_field_values . each { | c |
2012-01-07 00:34:42 +04:00
before = @custom_values_before_change [ c . custom_field_id ]
after = c . value
next if before == after || ( before . blank? && after . blank? )
2012-01-30 00:51:48 +04:00
if before . is_a? ( Array ) || after . is_a? ( Array )
before = [ before ] unless before . is_a? ( Array )
after = [ after ] unless after . is_a? ( Array )
# values removed
( before - after ) . reject ( & :blank? ) . each do | value |
@current_journal . details << JournalDetail . new ( :property = > 'cf' ,
:prop_key = > c . custom_field_id ,
:old_value = > value ,
:value = > nil )
end
# values added
( after - before ) . reject ( & :blank? ) . each do | value |
@current_journal . details << JournalDetail . new ( :property = > 'cf' ,
:prop_key = > c . custom_field_id ,
:old_value = > nil ,
:value = > value )
end
else
@current_journal . details << JournalDetail . new ( :property = > 'cf' ,
:prop_key = > c . custom_field_id ,
:old_value = > before ,
:value = > after )
end
2012-01-07 00:34:42 +04:00
}
end
2009-04-08 23:11:30 +04:00
@current_journal . save
2010-02-28 12:21:12 +03:00
# reset current journal
init_journal @current_journal . user , @current_journal . notes
2009-04-08 23:11:30 +04:00
end
end
2010-02-04 20:24:33 +03:00
# Query generator for selecting groups of issue counts for a project
# based on specific criteria
#
# Options
# * project - Project to search in.
# * field - String. Issue field to key off of in the grouping.
# * joins - String. The table name to join against.
def self . count_and_group_by ( options )
project = options . delete ( :project )
select_field = options . delete ( :field )
joins = options . delete ( :joins )
2011-04-08 16:21:06 +04:00
where = " #{ Issue . table_name } . #{ select_field } =j.id "
2011-05-17 06:14:06 +04:00
2010-02-04 20:24:33 +03:00
ActiveRecord :: Base . connection . select_all ( " select s.id as status_id,
s . is_closed as closed ,
j . id as #{select_field},
2011-04-08 16:21:06 +04:00
count ( #{Issue.table_name}.id) as total
2010-02-04 20:24:33 +03:00
from
2011-04-08 16:21:06 +04:00
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
2010-02-04 20:24:33 +03:00
where
2011-04-08 16:21:06 +04:00
#{Issue.table_name}.status_id=s.id
2010-02-04 20:24:33 +03:00
and #{where}
2011-04-08 16:21:06 +04:00
and #{Issue.table_name}.project_id=#{Project.table_name}.id
and #{visible_condition(User.current, :project => project)}
2010-02-04 20:24:33 +03:00
group by s . id , s . is_closed , j . id " )
end
2006-06-28 22:11:03 +04:00
end