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