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