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