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