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