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