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