##// END OF EJS Templates
Merged r14670 (#20677)....
Jean-Philippe Lang -
r14418:dd4a20b3a95e
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

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