2011-02-27 16:34:41 +03:00
# Redmine - project management software
# Copyright (C) 2006-2011 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
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
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 " ] ,
:include = > [ :project , :journals ] ,
# 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
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
2007-09-15 18:54:15 +04:00
validates_numericality_of :estimated_hours , :allow_nil = > true
2011-09-21 10:12:35 +04:00
validate :validate_issue
2007-03-12 20:59:02 +03:00
2009-01-27 21:19:27 +03:00
named_scope :visible , lambda { | * args | { :include = > :project ,
2011-04-05 16:50:19 +04:00
:conditions = > Issue . visible_condition ( args . shift || User . current , * args ) } }
2011-05-17 06:14:06 +04:00
2009-03-17 20:30:14 +03:00
named_scope :open , :conditions = > [ " #{ IssueStatus . table_name } .is_closed = ? " , false ] , :include = > :status
2009-12-11 21:48:34 +03:00
2010-09-10 07:09:02 +04:00
named_scope :recently_updated , :order = > " #{ Issue . table_name } .updated_on DESC "
2010-03-10 08:10:43 +03:00
named_scope :with_limit , lambda { | limit | { :limit = > limit } }
named_scope :on_active_project , :include = > [ :status , :project , :tracker ] ,
:conditions = > [ " #{ Project . table_name } .status= #{ Project :: STATUS_ACTIVE } " ]
2010-09-10 07:09:02 +04:00
named_scope :without_version , lambda {
{
:conditions = > { :fixed_version_id = > nil }
}
}
named_scope :with_query , lambda { | query |
{
:conditions = > Query . merge_conditions ( query . statement )
}
}
2010-03-10 08:10:43 +03:00
2010-02-28 12:21:12 +03:00
before_create :default_assign
2010-10-19 23:16:50 +04:00
before_save :close_duplicates , :update_done_ratio_from_issue_status
after_save :reschedule_following_issues , :update_nested_set_attributes , :update_parent_attributes , :create_journal
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 |
case role . issues_visibility
2011-04-15 17:23:13 +04:00
when 'all'
2011-04-11 21:53:15 +04:00
nil
2011-04-15 17:23:13 +04:00
when 'default'
2011-07-23 22:18:13 +04:00
user_ids = [ user . id ] + user . groups . map ( & :id )
2011-11-11 16:22:47 +04: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 ( ',' ) } )) "
2011-04-11 21:53:15 +04:00
when 'own'
2011-07-23 22:18:13 +04:00
user_ids = [ user . id ] + user . groups . map ( & :id )
2011-11-11 16:22:47 +04:00
" ( #{ table_name } .author_id = #{ user . id } OR #{ table_name } .assigned_to_id IN ( #{ user_ids . join ( ',' ) } )) "
2011-04-11 21:53:15 +04:00
else
'1=0'
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 |
case role . issues_visibility
2011-04-15 17:23:13 +04:00
when 'all'
2011-04-11 21:53:15 +04:00
true
2011-04-15 17:23:13 +04:00
when 'default'
2011-07-23 22:18:13 +04:00
! self . is_private? || self . author == user || user . is_or_belongs_to? ( assigned_to )
2011-04-11 21:53:15 +04:00
when 'own'
2011-07-23 22:18:13 +04:00
self . author == user || user . is_or_belongs_to? ( assigned_to )
2011-04-11 21:53:15 +04:00
else
false
end
end
2009-01-31 16:22:29 +03:00
end
2011-05-17 06:14:06 +04:00
2007-10-05 21:44:15 +04:00
def after_initialize
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
2007-10-05 21:44:15 +04:00
end
end
2011-05-17 06:14:06 +04:00
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
2007-10-28 17:31:59 +03:00
def copy_from ( arg )
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
2007-10-28 17:31:59 +03:00
self
end
2011-05-17 06:14:06 +04:00
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
2010-03-13 17:56:49 +03:00
def move_to_project ( * args )
2010-03-12 17:51:33 +03:00
ret = Issue . transaction do
2010-03-13 17:56:49 +03:00
move_to_project_without_transaction ( * args ) || raise ( ActiveRecord :: Rollback )
end || false
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
def move_to_project_without_transaction ( new_project , new_tracker = nil , options = { } )
options || = { }
issue = options [ :copy ] ? self . class . new . copy_from ( self ) : self
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
if new_project && issue . project_id != new_project . id
# delete issue relations
unless Setting . cross_project_issue_relations?
issue . relations_from . clear
issue . relations_to . clear
2007-11-17 20:45:21 +03:00
end
2010-03-13 17:56:49 +03:00
# issue is moved to another project
# reassign to the category with same name if any
new_category = issue . category . nil? ? nil : new_project . issue_categories . find_by_name ( issue . category . name )
issue . category = new_category
# Keep the fixed_version if it's still valid in the new_project
unless new_project . shared_versions . include? ( issue . fixed_version )
issue . fixed_version = nil
2009-12-05 01:46:12 +03:00
end
2010-03-13 17:56:49 +03:00
issue . project = new_project
if issue . parent && issue . parent . project_id != issue . project_id
issue . parent_issue_id = nil
2009-01-25 16:12:56 +03:00
end
2010-03-13 17:56:49 +03:00
end
if new_tracker
issue . tracker = new_tracker
issue . reset_custom_values!
end
if options [ :copy ]
2011-04-29 16:41:07 +04:00
issue . author = User . current
2010-03-13 17:56:49 +03:00
issue . custom_field_values = self . custom_field_values . inject ( { } ) { | h , v | h [ v . custom_field_id ] = v . value ; h }
issue . status = if options [ :attributes ] && options [ :attributes ] [ :status_id ]
IssueStatus . find_by_id ( options [ :attributes ] [ :status_id ] )
else
self . status
end
end
# Allow bulk setting of attributes on the issue
if options [ :attributes ]
issue . attributes = options [ :attributes ]
end
if issue . save
2011-04-29 17:27:50 +04:00
if options [ :copy ]
if current_journal && current_journal . notes . present?
issue . init_journal ( current_journal . user , current_journal . notes )
issue . current_journal . notify = false
issue . save
end
else
2010-03-13 17:56:49 +03:00
# Manually update project_id on related time entries
TimeEntry . update_all ( " project_id = #{ new_project . id } " , { :issue_id = > id } )
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
issue . children . each do | child |
unless child . move_to_project_without_transaction ( new_project )
# Move failed and transaction was rollback'd
return false
end
2009-01-25 16:12:56 +03:00
end
2007-11-17 20:45:21 +03:00
end
2010-03-13 17:56:49 +03:00
else
return false
2007-11-17 20:45:21 +03:00
end
2010-03-13 17:56:49 +03:00
issue
2007-11-17 20:45:21 +03:00
end
2010-03-20 20:37:04 +03:00
def status_id = ( sid )
self . status = nil
write_attribute ( :status_id , sid )
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
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
result
2009-11-29 22:46:40 +03:00
end
2011-08-21 12:46:10 +04:00
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
2009-12-09 12:12:29 +03:00
# Overrides attributes= so that tracker_id gets assigned first
def attributes_with_tracker_first = ( new_attributes , * args )
return if new_attributes . nil?
new_tracker_id = new_attributes [ 'tracker_id' ] || new_attributes [ :tracker_id ]
if new_tracker_id
self . tracker_id = new_tracker_id
end
2010-01-09 13:27:05 +03:00
send :attributes_without_tracker_first = , new_attributes , * 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)
alias_method_chain ( :attributes = , :tracker_first ) unless method_defined? ( :attributes_without_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
2010-12-12 16:11:53 +03:00
safe_attributes 'tracker_id' ,
'status_id' ,
'parent_issue_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' ,
: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' ,
:if = > lambda { | issue , user | issue . new_statuses_allowed_to ( user ) . any? }
2010-11-11 19:37:16 +03:00
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
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
# TODO: move workflow/permission checks from controllers to here
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
2010-11-11 19:37:16 +03:00
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
2010-12-12 16:11:53 +03:00
attrs = delete_unsafe_attributes ( attrs , user )
2011-05-17 06:14:06 +04:00
return if attrs . empty?
2010-11-12 14:34:53 +03:00
# Tracker must be set before since new_statuses_allowed_to depends on it.
if t = attrs . delete ( 'tracker_id' )
self . tracker_id = t
end
2011-05-17 06:14:06 +04:00
2010-02-24 00:26:29 +03:00
if attrs [ 'status_id' ]
2010-11-12 14:34:53 +03:00
unless new_statuses_allowed_to ( user ) . collect ( & :id ) . include? ( attrs [ 'status_id' ] . to_i )
2010-02-24 00:26:29 +03:00
attrs . delete ( 'status_id' )
end
end
2011-05-17 06:14:06 +04:00
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
2010-03-13 17:56:49 +03:00
if attrs . has_key? ( 'parent_issue_id' )
if ! user . allowed_to? ( :manage_subtasks , project )
attrs . delete ( 'parent_issue_id' )
elsif ! attrs [ 'parent_issue_id' ] . blank?
2010-11-20 13:20:53 +03:00
attrs . delete ( 'parent_issue_id' ) unless Issue . visible ( user ) . exists? ( attrs [ 'parent_issue_id' ] . to_i )
2010-03-13 17:56:49 +03:00
end
end
2011-05-17 06:14:06 +04:00
2010-02-24 00:26:29 +03:00
self . attributes = attrs
2010-01-12 23:17:20 +03:00
end
2011-05-17 06:14:06 +04:00
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
2007-03-12 20:59:02 +03:00
if self . due_date . nil? && @attributes [ 'due_date' ] && ! @attributes [ 'due_date' ] . empty?
2009-02-21 14:04:50 +03:00
errors . add :due_date , :not_a_date
2007-03-12 20:59:02 +03:00
end
2011-05-17 06:14:06 +04:00
2007-03-12 20:59:02 +03:00
if self . due_date and self . start_date and self . due_date < self . 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
if @parent_issue
if @parent_issue . project_id != project_id
errors . add :parent_issue_id , :not_same_project
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
errors . add :parent_issue_id , :not_a_valid_parent
end
end
end
2007-03-12 20:59:02 +03:00
end
2011-05-17 06:14:06 +04:00
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 )
@issue_before_change = self . clone
2008-02-17 18:02:54 +03:00
@issue_before_change . status = self . status
2007-03-12 20:59:02 +03:00
@custom_values_before_change = { }
self . custom_values . each { | c | @custom_values_before_change . store c . custom_field_id , c . value }
2008-08-25 16:51:29 +04:00
# Make sure updated_on is updated when adding a note.
updated_on_will_change!
2007-03-12 20:59:02 +03:00
@current_journal
end
2011-05-17 06:14:06 +04:00
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
2009-12-06 13:28:20 +03:00
@assignable_versions || = ( project . shared_versions . open + [ Version . find_by_id ( fixed_version_id_was ) ] ) . compact . 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
2008-01-06 20:06:14 +03:00
# Returns an array of status that user is able to apply
2010-03-04 03:58:52 +03:00
def new_statuses_allowed_to ( user , include_default = false )
2011-02-20 18:38:07 +03:00
statuses = status . find_new_statuses_allowed_to (
user . roles_for_project ( project ) ,
tracker ,
author == user ,
assigned_to_id_changed? ? assigned_to_id_was == user . id : assigned_to_id == user . id
)
2008-01-06 20:06:14 +03:00
statuses << status unless statuses . empty?
2010-03-04 03:58:52 +03:00
statuses << IssueStatus . default if include_default
2009-07-04 16:07:03 +04:00
statuses = statuses . uniq . sort
blocked? ? statuses . reject { | s | s . is_closed? } : statuses
2008-01-06 20:06:14 +03:00
end
2011-05-17 06:14:06 +04:00
2009-12-04 00:28:14 +03:00
# Returns the mail adresses of users that should be notified
2007-10-20 16:47:05 +04:00
def recipients
2009-12-04 00:28:14 +03:00
notified = project . notified_users
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
notified << author if author && author . active? && author . notify_about? ( self )
2011-07-23 22:18:13 +04:00
if assigned_to
if assigned_to . is_a? ( Group )
notified += assigned_to . users . select { | u | u . active? && u . notify_about? ( self ) }
else
notified << assigned_to if assigned_to . active? && assigned_to . notify_about? ( self )
end
end
2009-12-04 00:28:14 +03:00
notified . uniq!
# Remove users that can not view the issue
notified . reject! { | user | ! visible? ( user ) }
notified . collect ( & :mail )
end
2011-05-17 06:14:06 +04:00
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
2007-03-23 15:22:31 +03:00
def spent_hours
2010-03-13 17:56:49 +03:00
@spent_hours || = self_and_descendants . sum ( " #{ TimeEntry . table_name } .hours " , :include = > :time_entries ) . 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
2011-07-24 19:34:41 +04:00
@relations || = ( relations_from + relations_to ) . sort
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-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
2007-05-05 17:22:27 +04:00
def soonest_start
2010-03-13 18:29:34 +03:00
@soonest_start || = (
relations_to . collect { | relation | relation . successor_soonest_start } +
ancestors . collect ( & :soonest_start )
) . compact . max
end
2011-05-17 06:14:06 +04:00
2010-03-13 18:29:34 +03:00
def reschedule_after ( date )
return if date . nil?
if leaf?
if start_date . nil? || start_date < date
self . start_date , self . due_date = date , date + duration
save
end
else
leaves . each do | leaf |
leaf . reschedule_after ( date )
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
s = " issue status- #{ status . position } priority- #{ priority . position } "
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
2010-03-05 20:11:50 +03:00
# Saves an issue, time_entry, attachments, and a journal from the parameters
2010-04-11 20:27:37 +04:00
# Returns false if save fails
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
@time_entry . spent_on = Date . today
@time_entry . attributes = params [ :time_entry ]
self . time_entries << @time_entry
end
2011-05-17 06:14:06 +04:00
2010-04-11 20:48:46 +04:00
if valid?
attachments = Attachment . attach_files ( self , params [ :attachments ] )
# TODO: Rename hook
Redmine :: Hook . call_hook ( :controller_issues_edit_before_save , { :params = > params , :issue = > self , :time_entry = > @time_entry , :journal = > @current_journal } )
begin
if save
# TODO: Rename hook
Redmine :: Hook . call_hook ( :controller_issues_edit_after_save , { :params = > params , :issue = > self , :time_entry = > @time_entry , :journal = > @current_journal } )
else
raise ActiveRecord :: Rollback
end
rescue ActiveRecord :: StaleObjectError
attachments [ :files ] . each ( & :destroy )
2011-10-07 02:49:02 +04:00
errors . add :base , l ( :notice_locking_conflict )
2010-04-11 20:48:46 +04:00
raise ActiveRecord :: Rollback
2010-04-11 20:27:37 +04:00
end
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 )
parent_issue_id = arg . blank? ? nil : arg . to_i
if parent_issue_id && @parent_issue = Issue . find_by_id ( parent_issue_id )
@parent_issue . id
else
@parent_issue = nil
nil
end
end
2011-05-17 06:14:06 +04:00
2010-03-13 17:56:49 +03:00
def parent_issue_id
if instance_variable_defined? :@parent_issue
@parent_issue . nil? ? nil : @parent_issue . id
else
parent_id
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
2010-04-18 16:47:41 +04:00
# Returns an array of projects that current user can move issues to
def self . allowed_target_projects_on_move
projects = [ ]
if User . current . admin?
# admin is allowed to move issues to any active (visible) project
projects = Project . visible . all
elsif User . current . logged?
if Role . non_member . allowed_to? ( :move_issues )
projects = Project . visible . all
else
User . current . memberships . each { | m | projects << m . project if m . roles . detect { | r | r . allowed_to? ( :move_issues ) } }
end
end
projects
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
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
if priority_position = p . children . maximum ( " #{ IssuePriority . table_name } .position " , :include = > :priority )
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
done = p . leaves . sum ( " COALESCE(estimated_hours, #{ average } ) * (CASE WHEN is_closed = #{ connection . quoted_true } THEN 100 ELSE COALESCE(done_ratio, 0) END) " , :include = > :status ) . to_f
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
p . save ( false )
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
Issue . all ( :conditions = > merge_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' " ,
conditions ) ,
2009-12-06 13:28:20 +03:00
:include = > [ :project , :fixed_version ]
) . each do | issue |
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
2011-07-24 13:34:23 +04:00
# Callback on attachment deletion
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 )
journal = init_journal ( User . current )
journal . details << JournalDetail . new ( :property = > 'attachment' ,
:prop_key = > obj . id ,
:old_value = > obj . filename )
journal . save
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
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
2011-02-27 16:34:41 +03:00
( Issue . column_names - %w( id root_id lft rgt lock_version created_on updated_on ) ) . each { | c |
2011-07-02 15:05:27 +04:00
before = @issue_before_change . send ( c )
after = send ( c )
next if before == after || ( before . blank? && after . blank? )
2009-04-08 23:11:30 +04:00
@current_journal . details << JournalDetail . new ( :property = > 'attr' ,
:prop_key = > c ,
:old_value = > @issue_before_change . send ( c ) ,
2011-07-02 15:05:27 +04:00
:value = > send ( c ) )
2009-04-08 23:11:30 +04:00
}
# custom fields changes
custom_values . each { | c |
next if ( @custom_values_before_change [ c . custom_field_id ] == c . value ||
( @custom_values_before_change [ c . custom_field_id ] . blank? && c . value . blank? ) )
2011-05-17 06:14:06 +04:00
@current_journal . details << JournalDetail . new ( :property = > 'cf' ,
2009-04-08 23:11:30 +04:00
:prop_key = > c . custom_field_id ,
:old_value = > @custom_values_before_change [ c . custom_field_id ] ,
:value = > c . value )
2011-05-17 06:14:06 +04:00
}
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