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

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

@@ -1,1650 +1,1650
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 && Array(v.value).detect(&:present?).nil?
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 spent time for a collection of issues
950 # Preloads visible 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).where(:issue_id => issues.map(&:id)).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 # Preloads visible total spent time for a collection of issues
960 # Preloads visible total spent time for a collection of issues
961 def self.load_visible_total_spent_hours(issues, user=User.current)
961 def self.load_visible_total_spent_hours(issues, user=User.current)
962 if issues.any?
962 if issues.any?
963 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
963 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
964 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
964 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
965 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
965 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
966 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
966 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
967 issues.each do |issue|
967 issues.each do |issue|
968 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
968 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
969 end
969 end
970 end
970 end
971 end
971 end
972
972
973 # Preloads visible relations for a collection of issues
973 # Preloads visible relations for a collection of issues
974 def self.load_visible_relations(issues, user=User.current)
974 def self.load_visible_relations(issues, user=User.current)
975 if issues.any?
975 if issues.any?
976 issue_ids = issues.map(&:id)
976 issue_ids = issues.map(&:id)
977 # Relations with issue_from in given issues and visible issue_to
977 # Relations with issue_from in given issues and visible issue_to
978 relations_from = IssueRelation.joins(:issue_to => :project).
978 relations_from = IssueRelation.joins(:issue_to => :project).
979 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
979 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
980 # Relations with issue_to in given issues and visible issue_from
980 # Relations with issue_to in given issues and visible issue_from
981 relations_to = IssueRelation.joins(:issue_from => :project).
981 relations_to = IssueRelation.joins(:issue_from => :project).
982 where(visible_condition(user)).
982 where(visible_condition(user)).
983 where(:issue_to_id => issue_ids).to_a
983 where(:issue_to_id => issue_ids).to_a
984 issues.each do |issue|
984 issues.each do |issue|
985 relations =
985 relations =
986 relations_from.select {|relation| relation.issue_from_id == issue.id} +
986 relations_from.select {|relation| relation.issue_from_id == issue.id} +
987 relations_to.select {|relation| relation.issue_to_id == issue.id}
987 relations_to.select {|relation| relation.issue_to_id == issue.id}
988
988
989 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
989 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
990 end
990 end
991 end
991 end
992 end
992 end
993
993
994 # Finds an issue relation given its id.
994 # Finds an issue relation given its id.
995 def find_relation(relation_id)
995 def find_relation(relation_id)
996 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
996 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
997 end
997 end
998
998
999 # Returns all the other issues that depend on the issue
999 # Returns all the other issues that depend on the issue
1000 # The algorithm is a modified breadth first search (bfs)
1000 # The algorithm is a modified breadth first search (bfs)
1001 def all_dependent_issues(except=[])
1001 def all_dependent_issues(except=[])
1002 # The found dependencies
1002 # The found dependencies
1003 dependencies = []
1003 dependencies = []
1004
1004
1005 # The visited flag for every node (issue) used by the breadth first search
1005 # The visited flag for every node (issue) used by the breadth first search
1006 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
1006 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
1007
1007
1008 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
1008 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
1009 # the issue when it is processed.
1009 # the issue when it is processed.
1010
1010
1011 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
1011 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
1012 # but its children will not be added to the queue when it is processed.
1012 # but its children will not be added to the queue when it is processed.
1013
1013
1014 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
1014 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
1015 # the queue, but its children have not been added.
1015 # the queue, but its children have not been added.
1016
1016
1017 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
1017 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
1018 # the children still need to be processed.
1018 # the children still need to be processed.
1019
1019
1020 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
1020 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
1021 # added as dependent issues. It needs no further processing.
1021 # added as dependent issues. It needs no further processing.
1022
1022
1023 issue_status = Hash.new(eNOT_DISCOVERED)
1023 issue_status = Hash.new(eNOT_DISCOVERED)
1024
1024
1025 # The queue
1025 # The queue
1026 queue = []
1026 queue = []
1027
1027
1028 # Initialize the bfs, add start node (self) to the queue
1028 # Initialize the bfs, add start node (self) to the queue
1029 queue << self
1029 queue << self
1030 issue_status[self] = ePROCESS_ALL
1030 issue_status[self] = ePROCESS_ALL
1031
1031
1032 while (!queue.empty?) do
1032 while (!queue.empty?) do
1033 current_issue = queue.shift
1033 current_issue = queue.shift
1034 current_issue_status = issue_status[current_issue]
1034 current_issue_status = issue_status[current_issue]
1035 dependencies << current_issue
1035 dependencies << current_issue
1036
1036
1037 # Add parent to queue, if not already in it.
1037 # Add parent to queue, if not already in it.
1038 parent = current_issue.parent
1038 parent = current_issue.parent
1039 parent_status = issue_status[parent]
1039 parent_status = issue_status[parent]
1040
1040
1041 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
1041 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
1042 queue << parent
1042 queue << parent
1043 issue_status[parent] = ePROCESS_RELATIONS_ONLY
1043 issue_status[parent] = ePROCESS_RELATIONS_ONLY
1044 end
1044 end
1045
1045
1046 # Add children to queue, but only if they are not already in it and
1046 # Add children to queue, but only if they are not already in it and
1047 # the children of the current node need to be processed.
1047 # the children of the current node need to be processed.
1048 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
1048 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
1049 current_issue.children.each do |child|
1049 current_issue.children.each do |child|
1050 next if except.include?(child)
1050 next if except.include?(child)
1051
1051
1052 if (issue_status[child] == eNOT_DISCOVERED)
1052 if (issue_status[child] == eNOT_DISCOVERED)
1053 queue << child
1053 queue << child
1054 issue_status[child] = ePROCESS_ALL
1054 issue_status[child] = ePROCESS_ALL
1055 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1055 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1056 queue << child
1056 queue << child
1057 issue_status[child] = ePROCESS_CHILDREN_ONLY
1057 issue_status[child] = ePROCESS_CHILDREN_ONLY
1058 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1058 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1059 queue << child
1059 queue << child
1060 issue_status[child] = ePROCESS_ALL
1060 issue_status[child] = ePROCESS_ALL
1061 end
1061 end
1062 end
1062 end
1063 end
1063 end
1064
1064
1065 # Add related issues to the queue, if they are not already in it.
1065 # Add related issues to the queue, if they are not already in it.
1066 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1066 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1067 next if except.include?(related_issue)
1067 next if except.include?(related_issue)
1068
1068
1069 if (issue_status[related_issue] == eNOT_DISCOVERED)
1069 if (issue_status[related_issue] == eNOT_DISCOVERED)
1070 queue << related_issue
1070 queue << related_issue
1071 issue_status[related_issue] = ePROCESS_ALL
1071 issue_status[related_issue] = ePROCESS_ALL
1072 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1072 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1073 queue << related_issue
1073 queue << related_issue
1074 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1074 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1075 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1075 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1076 queue << related_issue
1076 queue << related_issue
1077 issue_status[related_issue] = ePROCESS_ALL
1077 issue_status[related_issue] = ePROCESS_ALL
1078 end
1078 end
1079 end
1079 end
1080
1080
1081 # Set new status for current issue
1081 # Set new status for current issue
1082 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1082 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1083 issue_status[current_issue] = eALL_PROCESSED
1083 issue_status[current_issue] = eALL_PROCESSED
1084 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1084 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1085 issue_status[current_issue] = eRELATIONS_PROCESSED
1085 issue_status[current_issue] = eRELATIONS_PROCESSED
1086 end
1086 end
1087 end # while
1087 end # while
1088
1088
1089 # Remove the issues from the "except" parameter from the result array
1089 # Remove the issues from the "except" parameter from the result array
1090 dependencies -= except
1090 dependencies -= except
1091 dependencies.delete(self)
1091 dependencies.delete(self)
1092
1092
1093 dependencies
1093 dependencies
1094 end
1094 end
1095
1095
1096 # Returns an array of issues that duplicate this one
1096 # Returns an array of issues that duplicate this one
1097 def duplicates
1097 def duplicates
1098 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1098 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1099 end
1099 end
1100
1100
1101 # Returns the due date or the target due date if any
1101 # Returns the due date or the target due date if any
1102 # Used on gantt chart
1102 # Used on gantt chart
1103 def due_before
1103 def due_before
1104 due_date || (fixed_version ? fixed_version.effective_date : nil)
1104 due_date || (fixed_version ? fixed_version.effective_date : nil)
1105 end
1105 end
1106
1106
1107 # Returns the time scheduled for this issue.
1107 # Returns the time scheduled for this issue.
1108 #
1108 #
1109 # Example:
1109 # Example:
1110 # Start Date: 2/26/09, End Date: 3/04/09
1110 # Start Date: 2/26/09, End Date: 3/04/09
1111 # duration => 6
1111 # duration => 6
1112 def duration
1112 def duration
1113 (start_date && due_date) ? due_date - start_date : 0
1113 (start_date && due_date) ? due_date - start_date : 0
1114 end
1114 end
1115
1115
1116 # Returns the duration in working days
1116 # Returns the duration in working days
1117 def working_duration
1117 def working_duration
1118 (start_date && due_date) ? working_days(start_date, due_date) : 0
1118 (start_date && due_date) ? working_days(start_date, due_date) : 0
1119 end
1119 end
1120
1120
1121 def soonest_start(reload=false)
1121 def soonest_start(reload=false)
1122 if @soonest_start.nil? || reload
1122 if @soonest_start.nil? || reload
1123 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1123 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1124 p = @parent_issue || parent
1124 p = @parent_issue || parent
1125 if p && Setting.parent_issue_dates == 'derived'
1125 if p && Setting.parent_issue_dates == 'derived'
1126 dates << p.soonest_start
1126 dates << p.soonest_start
1127 end
1127 end
1128 @soonest_start = dates.compact.max
1128 @soonest_start = dates.compact.max
1129 end
1129 end
1130 @soonest_start
1130 @soonest_start
1131 end
1131 end
1132
1132
1133 # Sets start_date on the given date or the next working day
1133 # Sets start_date on the given date or the next working day
1134 # and changes due_date to keep the same working duration.
1134 # and changes due_date to keep the same working duration.
1135 def reschedule_on(date)
1135 def reschedule_on(date)
1136 wd = working_duration
1136 wd = working_duration
1137 date = next_working_date(date)
1137 date = next_working_date(date)
1138 self.start_date = date
1138 self.start_date = date
1139 self.due_date = add_working_days(date, wd)
1139 self.due_date = add_working_days(date, wd)
1140 end
1140 end
1141
1141
1142 # Reschedules the issue on the given date or the next working day and saves the record.
1142 # Reschedules the issue on the given date or the next working day and saves the record.
1143 # If the issue is a parent task, this is done by rescheduling its subtasks.
1143 # If the issue is a parent task, this is done by rescheduling its subtasks.
1144 def reschedule_on!(date)
1144 def reschedule_on!(date)
1145 return if date.nil?
1145 return if date.nil?
1146 if leaf? || !dates_derived?
1146 if leaf? || !dates_derived?
1147 if start_date.nil? || start_date != date
1147 if start_date.nil? || start_date != date
1148 if start_date && start_date > date
1148 if start_date && start_date > date
1149 # Issue can not be moved earlier than its soonest start date
1149 # Issue can not be moved earlier than its soonest start date
1150 date = [soonest_start(true), date].compact.max
1150 date = [soonest_start(true), date].compact.max
1151 end
1151 end
1152 reschedule_on(date)
1152 reschedule_on(date)
1153 begin
1153 begin
1154 save
1154 save
1155 rescue ActiveRecord::StaleObjectError
1155 rescue ActiveRecord::StaleObjectError
1156 reload
1156 reload
1157 reschedule_on(date)
1157 reschedule_on(date)
1158 save
1158 save
1159 end
1159 end
1160 end
1160 end
1161 else
1161 else
1162 leaves.each do |leaf|
1162 leaves.each do |leaf|
1163 if leaf.start_date
1163 if leaf.start_date
1164 # Only move subtask if it starts at the same date as the parent
1164 # Only move subtask if it starts at the same date as the parent
1165 # or if it starts before the given date
1165 # or if it starts before the given date
1166 if start_date == leaf.start_date || date > leaf.start_date
1166 if start_date == leaf.start_date || date > leaf.start_date
1167 leaf.reschedule_on!(date)
1167 leaf.reschedule_on!(date)
1168 end
1168 end
1169 else
1169 else
1170 leaf.reschedule_on!(date)
1170 leaf.reschedule_on!(date)
1171 end
1171 end
1172 end
1172 end
1173 end
1173 end
1174 end
1174 end
1175
1175
1176 def dates_derived?
1176 def dates_derived?
1177 !leaf? && Setting.parent_issue_dates == 'derived'
1177 !leaf? && Setting.parent_issue_dates == 'derived'
1178 end
1178 end
1179
1179
1180 def priority_derived?
1180 def priority_derived?
1181 !leaf? && Setting.parent_issue_priority == 'derived'
1181 !leaf? && Setting.parent_issue_priority == 'derived'
1182 end
1182 end
1183
1183
1184 def done_ratio_derived?
1184 def done_ratio_derived?
1185 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1185 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1186 end
1186 end
1187
1187
1188 def <=>(issue)
1188 def <=>(issue)
1189 if issue.nil?
1189 if issue.nil?
1190 -1
1190 -1
1191 elsif root_id != issue.root_id
1191 elsif root_id != issue.root_id
1192 (root_id || 0) <=> (issue.root_id || 0)
1192 (root_id || 0) <=> (issue.root_id || 0)
1193 else
1193 else
1194 (lft || 0) <=> (issue.lft || 0)
1194 (lft || 0) <=> (issue.lft || 0)
1195 end
1195 end
1196 end
1196 end
1197
1197
1198 def to_s
1198 def to_s
1199 "#{tracker} ##{id}: #{subject}"
1199 "#{tracker} ##{id}: #{subject}"
1200 end
1200 end
1201
1201
1202 # Returns a string of css classes that apply to the issue
1202 # Returns a string of css classes that apply to the issue
1203 def css_classes(user=User.current)
1203 def css_classes(user=User.current)
1204 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1204 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1205 s << ' closed' if closed?
1205 s << ' closed' if closed?
1206 s << ' overdue' if overdue?
1206 s << ' overdue' if overdue?
1207 s << ' child' if child?
1207 s << ' child' if child?
1208 s << ' parent' unless leaf?
1208 s << ' parent' unless leaf?
1209 s << ' private' if is_private?
1209 s << ' private' if is_private?
1210 if user.logged?
1210 if user.logged?
1211 s << ' created-by-me' if author_id == user.id
1211 s << ' created-by-me' if author_id == user.id
1212 s << ' assigned-to-me' if assigned_to_id == user.id
1212 s << ' assigned-to-me' if assigned_to_id == user.id
1213 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1213 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1214 end
1214 end
1215 s
1215 s
1216 end
1216 end
1217
1217
1218 # Unassigns issues from +version+ if it's no longer shared with issue's project
1218 # Unassigns issues from +version+ if it's no longer shared with issue's project
1219 def self.update_versions_from_sharing_change(version)
1219 def self.update_versions_from_sharing_change(version)
1220 # Update issues assigned to the version
1220 # Update issues assigned to the version
1221 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1221 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1222 end
1222 end
1223
1223
1224 # Unassigns issues from versions that are no longer shared
1224 # Unassigns issues from versions that are no longer shared
1225 # after +project+ was moved
1225 # after +project+ was moved
1226 def self.update_versions_from_hierarchy_change(project)
1226 def self.update_versions_from_hierarchy_change(project)
1227 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1227 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1228 # Update issues of the moved projects and issues assigned to a version of a moved project
1228 # Update issues of the moved projects and issues assigned to a version of a moved project
1229 Issue.update_versions(
1229 Issue.update_versions(
1230 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1230 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1231 moved_project_ids, moved_project_ids]
1231 moved_project_ids, moved_project_ids]
1232 )
1232 )
1233 end
1233 end
1234
1234
1235 def parent_issue_id=(arg)
1235 def parent_issue_id=(arg)
1236 s = arg.to_s.strip.presence
1236 s = arg.to_s.strip.presence
1237 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1237 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1238 @invalid_parent_issue_id = nil
1238 @invalid_parent_issue_id = nil
1239 elsif s.blank?
1239 elsif s.blank?
1240 @parent_issue = nil
1240 @parent_issue = nil
1241 @invalid_parent_issue_id = nil
1241 @invalid_parent_issue_id = nil
1242 else
1242 else
1243 @parent_issue = nil
1243 @parent_issue = nil
1244 @invalid_parent_issue_id = arg
1244 @invalid_parent_issue_id = arg
1245 end
1245 end
1246 end
1246 end
1247
1247
1248 def parent_issue_id
1248 def parent_issue_id
1249 if @invalid_parent_issue_id
1249 if @invalid_parent_issue_id
1250 @invalid_parent_issue_id
1250 @invalid_parent_issue_id
1251 elsif instance_variable_defined? :@parent_issue
1251 elsif instance_variable_defined? :@parent_issue
1252 @parent_issue.nil? ? nil : @parent_issue.id
1252 @parent_issue.nil? ? nil : @parent_issue.id
1253 else
1253 else
1254 parent_id
1254 parent_id
1255 end
1255 end
1256 end
1256 end
1257
1257
1258 def set_parent_id
1258 def set_parent_id
1259 self.parent_id = parent_issue_id
1259 self.parent_id = parent_issue_id
1260 end
1260 end
1261
1261
1262 # Returns true if issue's project is a valid
1262 # Returns true if issue's project is a valid
1263 # parent issue project
1263 # parent issue project
1264 def valid_parent_project?(issue=parent)
1264 def valid_parent_project?(issue=parent)
1265 return true if issue.nil? || issue.project_id == project_id
1265 return true if issue.nil? || issue.project_id == project_id
1266
1266
1267 case Setting.cross_project_subtasks
1267 case Setting.cross_project_subtasks
1268 when 'system'
1268 when 'system'
1269 true
1269 true
1270 when 'tree'
1270 when 'tree'
1271 issue.project.root == project.root
1271 issue.project.root == project.root
1272 when 'hierarchy'
1272 when 'hierarchy'
1273 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1273 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1274 when 'descendants'
1274 when 'descendants'
1275 issue.project.is_or_is_ancestor_of?(project)
1275 issue.project.is_or_is_ancestor_of?(project)
1276 else
1276 else
1277 false
1277 false
1278 end
1278 end
1279 end
1279 end
1280
1280
1281 # Returns an issue scope based on project and scope
1281 # Returns an issue scope based on project and scope
1282 def self.cross_project_scope(project, scope=nil)
1282 def self.cross_project_scope(project, scope=nil)
1283 if project.nil?
1283 if project.nil?
1284 return Issue
1284 return Issue
1285 end
1285 end
1286 case scope
1286 case scope
1287 when 'all', 'system'
1287 when 'all', 'system'
1288 Issue
1288 Issue
1289 when 'tree'
1289 when 'tree'
1290 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1290 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1291 :lft => project.root.lft, :rgt => project.root.rgt)
1291 :lft => project.root.lft, :rgt => project.root.rgt)
1292 when 'hierarchy'
1292 when 'hierarchy'
1293 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)",
1293 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)",
1294 :lft => project.lft, :rgt => project.rgt)
1294 :lft => project.lft, :rgt => project.rgt)
1295 when 'descendants'
1295 when 'descendants'
1296 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1296 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1297 :lft => project.lft, :rgt => project.rgt)
1297 :lft => project.lft, :rgt => project.rgt)
1298 else
1298 else
1299 Issue.where(:project_id => project.id)
1299 Issue.where(:project_id => project.id)
1300 end
1300 end
1301 end
1301 end
1302
1302
1303 def self.by_tracker(project)
1303 def self.by_tracker(project)
1304 count_and_group_by(:project => project, :association => :tracker)
1304 count_and_group_by(:project => project, :association => :tracker)
1305 end
1305 end
1306
1306
1307 def self.by_version(project)
1307 def self.by_version(project)
1308 count_and_group_by(:project => project, :association => :fixed_version)
1308 count_and_group_by(:project => project, :association => :fixed_version)
1309 end
1309 end
1310
1310
1311 def self.by_priority(project)
1311 def self.by_priority(project)
1312 count_and_group_by(:project => project, :association => :priority)
1312 count_and_group_by(:project => project, :association => :priority)
1313 end
1313 end
1314
1314
1315 def self.by_category(project)
1315 def self.by_category(project)
1316 count_and_group_by(:project => project, :association => :category)
1316 count_and_group_by(:project => project, :association => :category)
1317 end
1317 end
1318
1318
1319 def self.by_assigned_to(project)
1319 def self.by_assigned_to(project)
1320 count_and_group_by(:project => project, :association => :assigned_to)
1320 count_and_group_by(:project => project, :association => :assigned_to)
1321 end
1321 end
1322
1322
1323 def self.by_author(project)
1323 def self.by_author(project)
1324 count_and_group_by(:project => project, :association => :author)
1324 count_and_group_by(:project => project, :association => :author)
1325 end
1325 end
1326
1326
1327 def self.by_subproject(project)
1327 def self.by_subproject(project)
1328 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1328 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1329 r.reject {|r| r["project_id"] == project.id.to_s}
1329 r.reject {|r| r["project_id"] == project.id.to_s}
1330 end
1330 end
1331
1331
1332 # Query generator for selecting groups of issue counts for a project
1332 # Query generator for selecting groups of issue counts for a project
1333 # based on specific criteria
1333 # based on specific criteria
1334 #
1334 #
1335 # Options
1335 # Options
1336 # * project - Project to search in.
1336 # * project - Project to search in.
1337 # * with_subprojects - Includes subprojects issues if set to true.
1337 # * with_subprojects - Includes subprojects issues if set to true.
1338 # * association - Symbol. Association for grouping.
1338 # * association - Symbol. Association for grouping.
1339 def self.count_and_group_by(options)
1339 def self.count_and_group_by(options)
1340 assoc = reflect_on_association(options[:association])
1340 assoc = reflect_on_association(options[:association])
1341 select_field = assoc.foreign_key
1341 select_field = assoc.foreign_key
1342
1342
1343 Issue.
1343 Issue.
1344 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1344 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1345 joins(:status, assoc.name).
1345 joins(:status, assoc.name).
1346 group(:status_id, :is_closed, select_field).
1346 group(:status_id, :is_closed, select_field).
1347 count.
1347 count.
1348 map do |columns, total|
1348 map do |columns, total|
1349 status_id, is_closed, field_value = columns
1349 status_id, is_closed, field_value = columns
1350 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1350 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1351 {
1351 {
1352 "status_id" => status_id.to_s,
1352 "status_id" => status_id.to_s,
1353 "closed" => is_closed,
1353 "closed" => is_closed,
1354 select_field => field_value.to_s,
1354 select_field => field_value.to_s,
1355 "total" => total.to_s
1355 "total" => total.to_s
1356 }
1356 }
1357 end
1357 end
1358 end
1358 end
1359
1359
1360 # Returns a scope of projects that user can assign the issue to
1360 # Returns a scope of projects that user can assign the issue to
1361 def allowed_target_projects(user=User.current)
1361 def allowed_target_projects(user=User.current)
1362 current_project = new_record? ? nil : project
1362 current_project = new_record? ? nil : project
1363 self.class.allowed_target_projects(user, current_project)
1363 self.class.allowed_target_projects(user, current_project)
1364 end
1364 end
1365
1365
1366 # Returns a scope of projects that user can assign issues to
1366 # Returns a scope of projects that user can assign issues to
1367 # If current_project is given, it will be included in the scope
1367 # If current_project is given, it will be included in the scope
1368 def self.allowed_target_projects(user=User.current, current_project=nil)
1368 def self.allowed_target_projects(user=User.current, current_project=nil)
1369 condition = Project.allowed_to_condition(user, :add_issues)
1369 condition = Project.allowed_to_condition(user, :add_issues)
1370 if current_project
1370 if current_project
1371 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1371 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1372 end
1372 end
1373 Project.where(condition)
1373 Project.where(condition)
1374 end
1374 end
1375
1375
1376 private
1376 private
1377
1377
1378 def after_project_change
1378 def after_project_change
1379 # Update project_id on related time entries
1379 # Update project_id on related time entries
1380 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1380 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1381
1381
1382 # Delete issue relations
1382 # Delete issue relations
1383 unless Setting.cross_project_issue_relations?
1383 unless Setting.cross_project_issue_relations?
1384 relations_from.clear
1384 relations_from.clear
1385 relations_to.clear
1385 relations_to.clear
1386 end
1386 end
1387
1387
1388 # Move subtasks that were in the same project
1388 # Move subtasks that were in the same project
1389 children.each do |child|
1389 children.each do |child|
1390 next unless child.project_id == project_id_was
1390 next unless child.project_id == project_id_was
1391 # Change project and keep project
1391 # Change project and keep project
1392 child.send :project=, project, true
1392 child.send :project=, project, true
1393 unless child.save
1393 unless child.save
1394 raise ActiveRecord::Rollback
1394 raise ActiveRecord::Rollback
1395 end
1395 end
1396 end
1396 end
1397 end
1397 end
1398
1398
1399 # Callback for after the creation of an issue by copy
1399 # Callback for after the creation of an issue by copy
1400 # * adds a "copied to" relation with the copied issue
1400 # * adds a "copied to" relation with the copied issue
1401 # * copies subtasks from the copied issue
1401 # * copies subtasks from the copied issue
1402 def after_create_from_copy
1402 def after_create_from_copy
1403 return unless copy? && !@after_create_from_copy_handled
1403 return unless copy? && !@after_create_from_copy_handled
1404
1404
1405 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1405 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1406 if @current_journal
1406 if @current_journal
1407 @copied_from.init_journal(@current_journal.user)
1407 @copied_from.init_journal(@current_journal.user)
1408 end
1408 end
1409 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1409 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1410 unless relation.save
1410 unless relation.save
1411 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1411 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1412 end
1412 end
1413 end
1413 end
1414
1414
1415 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1415 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1416 copy_options = (@copy_options || {}).merge(:subtasks => false)
1416 copy_options = (@copy_options || {}).merge(:subtasks => false)
1417 copied_issue_ids = {@copied_from.id => self.id}
1417 copied_issue_ids = {@copied_from.id => self.id}
1418 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1418 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1419 # Do not copy self when copying an issue as a descendant of the copied issue
1419 # Do not copy self when copying an issue as a descendant of the copied issue
1420 next if child == self
1420 next if child == self
1421 # Do not copy subtasks of issues that were not copied
1421 # Do not copy subtasks of issues that were not copied
1422 next unless copied_issue_ids[child.parent_id]
1422 next unless copied_issue_ids[child.parent_id]
1423 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1423 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1424 unless child.visible?
1424 unless child.visible?
1425 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1425 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1426 next
1426 next
1427 end
1427 end
1428 copy = Issue.new.copy_from(child, copy_options)
1428 copy = Issue.new.copy_from(child, copy_options)
1429 if @current_journal
1429 if @current_journal
1430 copy.init_journal(@current_journal.user)
1430 copy.init_journal(@current_journal.user)
1431 end
1431 end
1432 copy.author = author
1432 copy.author = author
1433 copy.project = project
1433 copy.project = project
1434 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1434 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1435 unless copy.save
1435 unless copy.save
1436 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
1436 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
1437 next
1437 next
1438 end
1438 end
1439 copied_issue_ids[child.id] = copy.id
1439 copied_issue_ids[child.id] = copy.id
1440 end
1440 end
1441 end
1441 end
1442 @after_create_from_copy_handled = true
1442 @after_create_from_copy_handled = true
1443 end
1443 end
1444
1444
1445 def update_nested_set_attributes
1445 def update_nested_set_attributes
1446 if parent_id_changed?
1446 if parent_id_changed?
1447 update_nested_set_attributes_on_parent_change
1447 update_nested_set_attributes_on_parent_change
1448 end
1448 end
1449 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1449 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1450 end
1450 end
1451
1451
1452 # Updates the nested set for when an existing issue is moved
1452 # Updates the nested set for when an existing issue is moved
1453 def update_nested_set_attributes_on_parent_change
1453 def update_nested_set_attributes_on_parent_change
1454 former_parent_id = parent_id_was
1454 former_parent_id = parent_id_was
1455 # delete invalid relations of all descendants
1455 # delete invalid relations of all descendants
1456 self_and_descendants.each do |issue|
1456 self_and_descendants.each do |issue|
1457 issue.relations.each do |relation|
1457 issue.relations.each do |relation|
1458 relation.destroy unless relation.valid?
1458 relation.destroy unless relation.valid?
1459 end
1459 end
1460 end
1460 end
1461 # update former parent
1461 # update former parent
1462 recalculate_attributes_for(former_parent_id) if former_parent_id
1462 recalculate_attributes_for(former_parent_id) if former_parent_id
1463 end
1463 end
1464
1464
1465 def update_parent_attributes
1465 def update_parent_attributes
1466 if parent_id
1466 if parent_id
1467 recalculate_attributes_for(parent_id)
1467 recalculate_attributes_for(parent_id)
1468 association(:parent).reset
1468 association(:parent).reset
1469 end
1469 end
1470 end
1470 end
1471
1471
1472 def recalculate_attributes_for(issue_id)
1472 def recalculate_attributes_for(issue_id)
1473 if issue_id && p = Issue.find_by_id(issue_id)
1473 if issue_id && p = Issue.find_by_id(issue_id)
1474 if p.priority_derived?
1474 if p.priority_derived?
1475 # priority = highest priority of children
1475 # priority = highest priority of children
1476 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1476 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1477 p.priority = IssuePriority.find_by_position(priority_position)
1477 p.priority = IssuePriority.find_by_position(priority_position)
1478 end
1478 end
1479 end
1479 end
1480
1480
1481 if p.dates_derived?
1481 if p.dates_derived?
1482 # start/due dates = lowest/highest dates of children
1482 # start/due dates = lowest/highest dates of children
1483 p.start_date = p.children.minimum(:start_date)
1483 p.start_date = p.children.minimum(:start_date)
1484 p.due_date = p.children.maximum(:due_date)
1484 p.due_date = p.children.maximum(:due_date)
1485 if p.start_date && p.due_date && p.due_date < p.start_date
1485 if p.start_date && p.due_date && p.due_date < p.start_date
1486 p.start_date, p.due_date = p.due_date, p.start_date
1486 p.start_date, p.due_date = p.due_date, p.start_date
1487 end
1487 end
1488 end
1488 end
1489
1489
1490 if p.done_ratio_derived?
1490 if p.done_ratio_derived?
1491 # done ratio = weighted average ratio of leaves
1491 # done ratio = weighted average ratio of leaves
1492 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1492 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1493 leaves_count = p.leaves.count
1493 leaves_count = p.leaves.count
1494 if leaves_count > 0
1494 if leaves_count > 0
1495 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1495 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1496 if average == 0
1496 if average == 0
1497 average = 1
1497 average = 1
1498 end
1498 end
1499 done = p.leaves.joins(:status).
1499 done = p.leaves.joins(:status).
1500 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1500 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1501 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1501 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1502 progress = done / (average * leaves_count)
1502 progress = done / (average * leaves_count)
1503 p.done_ratio = progress.round
1503 p.done_ratio = progress.round
1504 end
1504 end
1505 end
1505 end
1506 end
1506 end
1507
1507
1508 # ancestors will be recursively updated
1508 # ancestors will be recursively updated
1509 p.save(:validate => false)
1509 p.save(:validate => false)
1510 end
1510 end
1511 end
1511 end
1512
1512
1513 # Update issues so their versions are not pointing to a
1513 # Update issues so their versions are not pointing to a
1514 # fixed_version that is not shared with the issue's project
1514 # fixed_version that is not shared with the issue's project
1515 def self.update_versions(conditions=nil)
1515 def self.update_versions(conditions=nil)
1516 # Only need to update issues with a fixed_version from
1516 # Only need to update issues with a fixed_version from
1517 # a different project and that is not systemwide shared
1517 # a different project and that is not systemwide shared
1518 Issue.joins(:project, :fixed_version).
1518 Issue.joins(:project, :fixed_version).
1519 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1519 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1520 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1520 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1521 " AND #{Version.table_name}.sharing <> 'system'").
1521 " AND #{Version.table_name}.sharing <> 'system'").
1522 where(conditions).each do |issue|
1522 where(conditions).each do |issue|
1523 next if issue.project.nil? || issue.fixed_version.nil?
1523 next if issue.project.nil? || issue.fixed_version.nil?
1524 unless issue.project.shared_versions.include?(issue.fixed_version)
1524 unless issue.project.shared_versions.include?(issue.fixed_version)
1525 issue.init_journal(User.current)
1525 issue.init_journal(User.current)
1526 issue.fixed_version = nil
1526 issue.fixed_version = nil
1527 issue.save
1527 issue.save
1528 end
1528 end
1529 end
1529 end
1530 end
1530 end
1531
1531
1532 # Callback on file attachment
1532 # Callback on file attachment
1533 def attachment_added(attachment)
1533 def attachment_added(attachment)
1534 if current_journal && !attachment.new_record?
1534 if current_journal && !attachment.new_record?
1535 current_journal.journalize_attachment(attachment, :added)
1535 current_journal.journalize_attachment(attachment, :added)
1536 end
1536 end
1537 end
1537 end
1538
1538
1539 # Callback on attachment deletion
1539 # Callback on attachment deletion
1540 def attachment_removed(attachment)
1540 def attachment_removed(attachment)
1541 if current_journal && !attachment.new_record?
1541 if current_journal && !attachment.new_record?
1542 current_journal.journalize_attachment(attachment, :removed)
1542 current_journal.journalize_attachment(attachment, :removed)
1543 current_journal.save
1543 current_journal.save
1544 end
1544 end
1545 end
1545 end
1546
1546
1547 # Called after a relation is added
1547 # Called after a relation is added
1548 def relation_added(relation)
1548 def relation_added(relation)
1549 if current_journal
1549 if current_journal
1550 current_journal.journalize_relation(relation, :added)
1550 current_journal.journalize_relation(relation, :added)
1551 current_journal.save
1551 current_journal.save
1552 end
1552 end
1553 end
1553 end
1554
1554
1555 # Called after a relation is removed
1555 # Called after a relation is removed
1556 def relation_removed(relation)
1556 def relation_removed(relation)
1557 if current_journal
1557 if current_journal
1558 current_journal.journalize_relation(relation, :removed)
1558 current_journal.journalize_relation(relation, :removed)
1559 current_journal.save
1559 current_journal.save
1560 end
1560 end
1561 end
1561 end
1562
1562
1563 # Default assignment based on category
1563 # Default assignment based on category
1564 def default_assign
1564 def default_assign
1565 if assigned_to.nil? && category && category.assigned_to
1565 if assigned_to.nil? && category && category.assigned_to
1566 self.assigned_to = category.assigned_to
1566 self.assigned_to = category.assigned_to
1567 end
1567 end
1568 end
1568 end
1569
1569
1570 # Updates start/due dates of following issues
1570 # Updates start/due dates of following issues
1571 def reschedule_following_issues
1571 def reschedule_following_issues
1572 if start_date_changed? || due_date_changed?
1572 if start_date_changed? || due_date_changed?
1573 relations_from.each do |relation|
1573 relations_from.each do |relation|
1574 relation.set_issue_to_dates
1574 relation.set_issue_to_dates
1575 end
1575 end
1576 end
1576 end
1577 end
1577 end
1578
1578
1579 # Closes duplicates if the issue is being closed
1579 # Closes duplicates if the issue is being closed
1580 def close_duplicates
1580 def close_duplicates
1581 if closing?
1581 if closing?
1582 duplicates.each do |duplicate|
1582 duplicates.each do |duplicate|
1583 # Reload is needed in case the duplicate was updated by a previous duplicate
1583 # Reload is needed in case the duplicate was updated by a previous duplicate
1584 duplicate.reload
1584 duplicate.reload
1585 # Don't re-close it if it's already closed
1585 # Don't re-close it if it's already closed
1586 next if duplicate.closed?
1586 next if duplicate.closed?
1587 # Same user and notes
1587 # Same user and notes
1588 if @current_journal
1588 if @current_journal
1589 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1589 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1590 end
1590 end
1591 duplicate.update_attribute :status, self.status
1591 duplicate.update_attribute :status, self.status
1592 end
1592 end
1593 end
1593 end
1594 end
1594 end
1595
1595
1596 # Make sure updated_on is updated when adding a note and set updated_on now
1596 # Make sure updated_on is updated when adding a note and set updated_on now
1597 # so we can set closed_on with the same value on closing
1597 # so we can set closed_on with the same value on closing
1598 def force_updated_on_change
1598 def force_updated_on_change
1599 if @current_journal || changed?
1599 if @current_journal || changed?
1600 self.updated_on = current_time_from_proper_timezone
1600 self.updated_on = current_time_from_proper_timezone
1601 if new_record?
1601 if new_record?
1602 self.created_on = updated_on
1602 self.created_on = updated_on
1603 end
1603 end
1604 end
1604 end
1605 end
1605 end
1606
1606
1607 # Callback for setting closed_on when the issue is closed.
1607 # Callback for setting closed_on when the issue is closed.
1608 # The closed_on attribute stores the time of the last closing
1608 # The closed_on attribute stores the time of the last closing
1609 # and is preserved when the issue is reopened.
1609 # and is preserved when the issue is reopened.
1610 def update_closed_on
1610 def update_closed_on
1611 if closing?
1611 if closing?
1612 self.closed_on = updated_on
1612 self.closed_on = updated_on
1613 end
1613 end
1614 end
1614 end
1615
1615
1616 # Saves the changes in a Journal
1616 # Saves the changes in a Journal
1617 # Called after_save
1617 # Called after_save
1618 def create_journal
1618 def create_journal
1619 if current_journal
1619 if current_journal
1620 current_journal.save
1620 current_journal.save
1621 end
1621 end
1622 end
1622 end
1623
1623
1624 def send_notification
1624 def send_notification
1625 if Setting.notified_events.include?('issue_added')
1625 if Setting.notified_events.include?('issue_added')
1626 Mailer.deliver_issue_add(self)
1626 Mailer.deliver_issue_add(self)
1627 end
1627 end
1628 end
1628 end
1629
1629
1630 # Stores the previous assignee so we can still have access
1630 # Stores the previous assignee so we can still have access
1631 # to it during after_save callbacks (assigned_to_id_was is reset)
1631 # to it during after_save callbacks (assigned_to_id_was is reset)
1632 def set_assigned_to_was
1632 def set_assigned_to_was
1633 @previous_assigned_to_id = assigned_to_id_was
1633 @previous_assigned_to_id = assigned_to_id_was
1634 end
1634 end
1635
1635
1636 # Clears the previous assignee at the end of after_save callbacks
1636 # Clears the previous assignee at the end of after_save callbacks
1637 def clear_assigned_to_was
1637 def clear_assigned_to_was
1638 @assigned_to_was = nil
1638 @assigned_to_was = nil
1639 @previous_assigned_to_id = nil
1639 @previous_assigned_to_id = nil
1640 end
1640 end
1641
1641
1642 def clear_disabled_fields
1642 def clear_disabled_fields
1643 if tracker
1643 if tracker
1644 tracker.disabled_core_fields.each do |attribute|
1644 tracker.disabled_core_fields.each do |attribute|
1645 send "#{attribute}=", nil
1645 send "#{attribute}=", nil
1646 end
1646 end
1647 self.done_ratio ||= 0
1647 self.done_ratio ||= 0
1648 end
1648 end
1649 end
1649 end
1650 end
1650 end
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now