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