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