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