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