##// END OF EJS Templates
remove trailing white-spaces excluding SQL from issue model source....
Toshi MARUYAMA -
r5690:42a367b2588e
parent child
Show More
@@ -1,920 +1,920
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_remove => :attachment_removed
38 acts_as_attachable :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61
61
62 named_scope :visible, lambda {|*args| { :include => :project,
62 named_scope :visible, lambda {|*args| { :include => :project,
63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64
64
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66
66
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71
71
72 named_scope :without_version, lambda {
72 named_scope :without_version, lambda {
73 {
73 {
74 :conditions => { :fixed_version_id => nil}
74 :conditions => { :fixed_version_id => nil}
75 }
75 }
76 }
76 }
77
77
78 named_scope :with_query, lambda {|query|
78 named_scope :with_query, lambda {|query|
79 {
79 {
80 :conditions => Query.merge_conditions(query.statement)
80 :conditions => Query.merge_conditions(query.statement)
81 }
81 }
82 }
82 }
83
83
84 before_create :default_assign
84 before_create :default_assign
85 before_save :close_duplicates, :update_done_ratio_from_issue_status
85 before_save :close_duplicates, :update_done_ratio_from_issue_status
86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
87 after_destroy :update_parent_attributes
87 after_destroy :update_parent_attributes
88
88
89 # Returns a SQL conditions string used to find all issues visible by the specified user
89 # Returns a SQL conditions string used to find all issues visible by the specified user
90 def self.visible_condition(user, options={})
90 def self.visible_condition(user, options={})
91 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
91 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
92 case role.issues_visibility
92 case role.issues_visibility
93 when 'all'
93 when 'all'
94 nil
94 nil
95 when 'default'
95 when 'default'
96 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
96 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
97 when 'own'
97 when 'own'
98 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
98 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
99 else
99 else
100 '1=0'
100 '1=0'
101 end
101 end
102 end
102 end
103 end
103 end
104
104
105 # Returns true if usr or current user is allowed to view the issue
105 # Returns true if usr or current user is allowed to view the issue
106 def visible?(usr=nil)
106 def visible?(usr=nil)
107 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
107 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
108 case role.issues_visibility
108 case role.issues_visibility
109 when 'all'
109 when 'all'
110 true
110 true
111 when 'default'
111 when 'default'
112 !self.is_private? || self.author == user || self.assigned_to == user
112 !self.is_private? || self.author == user || self.assigned_to == user
113 when 'own'
113 when 'own'
114 self.author == user || self.assigned_to == user
114 self.author == user || self.assigned_to == user
115 else
115 else
116 false
116 false
117 end
117 end
118 end
118 end
119 end
119 end
120
120
121 def after_initialize
121 def after_initialize
122 if new_record?
122 if new_record?
123 # set default values for new records only
123 # set default values for new records only
124 self.status ||= IssueStatus.default
124 self.status ||= IssueStatus.default
125 self.priority ||= IssuePriority.default
125 self.priority ||= IssuePriority.default
126 end
126 end
127 end
127 end
128
128
129 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
129 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
130 def available_custom_fields
130 def available_custom_fields
131 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
131 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
132 end
132 end
133
133
134 def copy_from(arg)
134 def copy_from(arg)
135 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
135 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
136 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
136 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
137 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
137 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
138 self.status = issue.status
138 self.status = issue.status
139 self
139 self
140 end
140 end
141
141
142 # Moves/copies an issue to a new project and tracker
142 # Moves/copies an issue to a new project and tracker
143 # Returns the moved/copied issue on success, false on failure
143 # Returns the moved/copied issue on success, false on failure
144 def move_to_project(*args)
144 def move_to_project(*args)
145 ret = Issue.transaction do
145 ret = Issue.transaction do
146 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
146 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
147 end || false
147 end || false
148 end
148 end
149
149
150 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
150 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
151 options ||= {}
151 options ||= {}
152 issue = options[:copy] ? self.class.new.copy_from(self) : self
152 issue = options[:copy] ? self.class.new.copy_from(self) : self
153
153
154 if new_project && issue.project_id != new_project.id
154 if new_project && issue.project_id != new_project.id
155 # delete issue relations
155 # delete issue relations
156 unless Setting.cross_project_issue_relations?
156 unless Setting.cross_project_issue_relations?
157 issue.relations_from.clear
157 issue.relations_from.clear
158 issue.relations_to.clear
158 issue.relations_to.clear
159 end
159 end
160 # issue is moved to another project
160 # issue is moved to another project
161 # reassign to the category with same name if any
161 # reassign to the category with same name if any
162 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
162 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
163 issue.category = new_category
163 issue.category = new_category
164 # Keep the fixed_version if it's still valid in the new_project
164 # Keep the fixed_version if it's still valid in the new_project
165 unless new_project.shared_versions.include?(issue.fixed_version)
165 unless new_project.shared_versions.include?(issue.fixed_version)
166 issue.fixed_version = nil
166 issue.fixed_version = nil
167 end
167 end
168 issue.project = new_project
168 issue.project = new_project
169 if issue.parent && issue.parent.project_id != issue.project_id
169 if issue.parent && issue.parent.project_id != issue.project_id
170 issue.parent_issue_id = nil
170 issue.parent_issue_id = nil
171 end
171 end
172 end
172 end
173 if new_tracker
173 if new_tracker
174 issue.tracker = new_tracker
174 issue.tracker = new_tracker
175 issue.reset_custom_values!
175 issue.reset_custom_values!
176 end
176 end
177 if options[:copy]
177 if options[:copy]
178 issue.author = User.current
178 issue.author = User.current
179 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
179 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
180 issue.status = if options[:attributes] && options[:attributes][:status_id]
180 issue.status = if options[:attributes] && options[:attributes][:status_id]
181 IssueStatus.find_by_id(options[:attributes][:status_id])
181 IssueStatus.find_by_id(options[:attributes][:status_id])
182 else
182 else
183 self.status
183 self.status
184 end
184 end
185 end
185 end
186 # Allow bulk setting of attributes on the issue
186 # Allow bulk setting of attributes on the issue
187 if options[:attributes]
187 if options[:attributes]
188 issue.attributes = options[:attributes]
188 issue.attributes = options[:attributes]
189 end
189 end
190 if issue.save
190 if issue.save
191 if options[:copy]
191 if options[:copy]
192 if current_journal && current_journal.notes.present?
192 if current_journal && current_journal.notes.present?
193 issue.init_journal(current_journal.user, current_journal.notes)
193 issue.init_journal(current_journal.user, current_journal.notes)
194 issue.current_journal.notify = false
194 issue.current_journal.notify = false
195 issue.save
195 issue.save
196 end
196 end
197 else
197 else
198 # Manually update project_id on related time entries
198 # Manually update project_id on related time entries
199 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
199 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
200
200
201 issue.children.each do |child|
201 issue.children.each do |child|
202 unless child.move_to_project_without_transaction(new_project)
202 unless child.move_to_project_without_transaction(new_project)
203 # Move failed and transaction was rollback'd
203 # Move failed and transaction was rollback'd
204 return false
204 return false
205 end
205 end
206 end
206 end
207 end
207 end
208 else
208 else
209 return false
209 return false
210 end
210 end
211 issue
211 issue
212 end
212 end
213
213
214 def status_id=(sid)
214 def status_id=(sid)
215 self.status = nil
215 self.status = nil
216 write_attribute(:status_id, sid)
216 write_attribute(:status_id, sid)
217 end
217 end
218
218
219 def priority_id=(pid)
219 def priority_id=(pid)
220 self.priority = nil
220 self.priority = nil
221 write_attribute(:priority_id, pid)
221 write_attribute(:priority_id, pid)
222 end
222 end
223
223
224 def tracker_id=(tid)
224 def tracker_id=(tid)
225 self.tracker = nil
225 self.tracker = nil
226 result = write_attribute(:tracker_id, tid)
226 result = write_attribute(:tracker_id, tid)
227 @custom_field_values = nil
227 @custom_field_values = nil
228 result
228 result
229 end
229 end
230
230
231 # Overrides attributes= so that tracker_id gets assigned first
231 # Overrides attributes= so that tracker_id gets assigned first
232 def attributes_with_tracker_first=(new_attributes, *args)
232 def attributes_with_tracker_first=(new_attributes, *args)
233 return if new_attributes.nil?
233 return if new_attributes.nil?
234 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
234 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
235 if new_tracker_id
235 if new_tracker_id
236 self.tracker_id = new_tracker_id
236 self.tracker_id = new_tracker_id
237 end
237 end
238 send :attributes_without_tracker_first=, new_attributes, *args
238 send :attributes_without_tracker_first=, new_attributes, *args
239 end
239 end
240 # Do not redefine alias chain on reload (see #4838)
240 # Do not redefine alias chain on reload (see #4838)
241 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
241 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
242
242
243 def estimated_hours=(h)
243 def estimated_hours=(h)
244 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
244 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
245 end
245 end
246
246
247 safe_attributes 'tracker_id',
247 safe_attributes 'tracker_id',
248 'status_id',
248 'status_id',
249 'parent_issue_id',
249 'parent_issue_id',
250 'category_id',
250 'category_id',
251 'assigned_to_id',
251 'assigned_to_id',
252 'priority_id',
252 'priority_id',
253 'fixed_version_id',
253 'fixed_version_id',
254 'subject',
254 'subject',
255 'description',
255 'description',
256 'start_date',
256 'start_date',
257 'due_date',
257 'due_date',
258 'done_ratio',
258 'done_ratio',
259 'estimated_hours',
259 'estimated_hours',
260 'custom_field_values',
260 'custom_field_values',
261 'custom_fields',
261 'custom_fields',
262 'lock_version',
262 'lock_version',
263 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
263 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
264
264
265 safe_attributes 'status_id',
265 safe_attributes 'status_id',
266 'assigned_to_id',
266 'assigned_to_id',
267 'fixed_version_id',
267 'fixed_version_id',
268 'done_ratio',
268 'done_ratio',
269 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
269 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
270
270
271 safe_attributes 'is_private',
271 safe_attributes 'is_private',
272 :if => lambda {|issue, user|
272 :if => lambda {|issue, user|
273 user.allowed_to?(:set_issues_private, issue.project) ||
273 user.allowed_to?(:set_issues_private, issue.project) ||
274 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
274 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
275 }
275 }
276
276
277 # Safely sets attributes
277 # Safely sets attributes
278 # Should be called from controllers instead of #attributes=
278 # Should be called from controllers instead of #attributes=
279 # attr_accessible is too rough because we still want things like
279 # attr_accessible is too rough because we still want things like
280 # Issue.new(:project => foo) to work
280 # Issue.new(:project => foo) to work
281 # TODO: move workflow/permission checks from controllers to here
281 # TODO: move workflow/permission checks from controllers to here
282 def safe_attributes=(attrs, user=User.current)
282 def safe_attributes=(attrs, user=User.current)
283 return unless attrs.is_a?(Hash)
283 return unless attrs.is_a?(Hash)
284
284
285 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
285 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
286 attrs = delete_unsafe_attributes(attrs, user)
286 attrs = delete_unsafe_attributes(attrs, user)
287 return if attrs.empty?
287 return if attrs.empty?
288
288
289 # Tracker must be set before since new_statuses_allowed_to depends on it.
289 # Tracker must be set before since new_statuses_allowed_to depends on it.
290 if t = attrs.delete('tracker_id')
290 if t = attrs.delete('tracker_id')
291 self.tracker_id = t
291 self.tracker_id = t
292 end
292 end
293
293
294 if attrs['status_id']
294 if attrs['status_id']
295 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
295 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
296 attrs.delete('status_id')
296 attrs.delete('status_id')
297 end
297 end
298 end
298 end
299
299
300 unless leaf?
300 unless leaf?
301 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
301 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
302 end
302 end
303
303
304 if attrs.has_key?('parent_issue_id')
304 if attrs.has_key?('parent_issue_id')
305 if !user.allowed_to?(:manage_subtasks, project)
305 if !user.allowed_to?(:manage_subtasks, project)
306 attrs.delete('parent_issue_id')
306 attrs.delete('parent_issue_id')
307 elsif !attrs['parent_issue_id'].blank?
307 elsif !attrs['parent_issue_id'].blank?
308 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
308 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
309 end
309 end
310 end
310 end
311
311
312 self.attributes = attrs
312 self.attributes = attrs
313 end
313 end
314
314
315 def done_ratio
315 def done_ratio
316 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
316 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
317 status.default_done_ratio
317 status.default_done_ratio
318 else
318 else
319 read_attribute(:done_ratio)
319 read_attribute(:done_ratio)
320 end
320 end
321 end
321 end
322
322
323 def self.use_status_for_done_ratio?
323 def self.use_status_for_done_ratio?
324 Setting.issue_done_ratio == 'issue_status'
324 Setting.issue_done_ratio == 'issue_status'
325 end
325 end
326
326
327 def self.use_field_for_done_ratio?
327 def self.use_field_for_done_ratio?
328 Setting.issue_done_ratio == 'issue_field'
328 Setting.issue_done_ratio == 'issue_field'
329 end
329 end
330
330
331 def validate
331 def validate
332 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
332 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
333 errors.add :due_date, :not_a_date
333 errors.add :due_date, :not_a_date
334 end
334 end
335
335
336 if self.due_date and self.start_date and self.due_date < self.start_date
336 if self.due_date and self.start_date and self.due_date < self.start_date
337 errors.add :due_date, :greater_than_start_date
337 errors.add :due_date, :greater_than_start_date
338 end
338 end
339
339
340 if start_date && soonest_start && start_date < soonest_start
340 if start_date && soonest_start && start_date < soonest_start
341 errors.add :start_date, :invalid
341 errors.add :start_date, :invalid
342 end
342 end
343
343
344 if fixed_version
344 if fixed_version
345 if !assignable_versions.include?(fixed_version)
345 if !assignable_versions.include?(fixed_version)
346 errors.add :fixed_version_id, :inclusion
346 errors.add :fixed_version_id, :inclusion
347 elsif reopened? && fixed_version.closed?
347 elsif reopened? && fixed_version.closed?
348 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
348 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
349 end
349 end
350 end
350 end
351
351
352 # Checks that the issue can not be added/moved to a disabled tracker
352 # Checks that the issue can not be added/moved to a disabled tracker
353 if project && (tracker_id_changed? || project_id_changed?)
353 if project && (tracker_id_changed? || project_id_changed?)
354 unless project.trackers.include?(tracker)
354 unless project.trackers.include?(tracker)
355 errors.add :tracker_id, :inclusion
355 errors.add :tracker_id, :inclusion
356 end
356 end
357 end
357 end
358
358
359 # Checks parent issue assignment
359 # Checks parent issue assignment
360 if @parent_issue
360 if @parent_issue
361 if @parent_issue.project_id != project_id
361 if @parent_issue.project_id != project_id
362 errors.add :parent_issue_id, :not_same_project
362 errors.add :parent_issue_id, :not_same_project
363 elsif !new_record?
363 elsif !new_record?
364 # moving an existing issue
364 # moving an existing issue
365 if @parent_issue.root_id != root_id
365 if @parent_issue.root_id != root_id
366 # we can always move to another tree
366 # we can always move to another tree
367 elsif move_possible?(@parent_issue)
367 elsif move_possible?(@parent_issue)
368 # move accepted inside tree
368 # move accepted inside tree
369 else
369 else
370 errors.add :parent_issue_id, :not_a_valid_parent
370 errors.add :parent_issue_id, :not_a_valid_parent
371 end
371 end
372 end
372 end
373 end
373 end
374 end
374 end
375
375
376 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
376 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
377 # even if the user turns off the setting later
377 # even if the user turns off the setting later
378 def update_done_ratio_from_issue_status
378 def update_done_ratio_from_issue_status
379 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
379 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
380 self.done_ratio = status.default_done_ratio
380 self.done_ratio = status.default_done_ratio
381 end
381 end
382 end
382 end
383
383
384 def init_journal(user, notes = "")
384 def init_journal(user, notes = "")
385 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
385 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
386 @issue_before_change = self.clone
386 @issue_before_change = self.clone
387 @issue_before_change.status = self.status
387 @issue_before_change.status = self.status
388 @custom_values_before_change = {}
388 @custom_values_before_change = {}
389 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
389 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
390 # Make sure updated_on is updated when adding a note.
390 # Make sure updated_on is updated when adding a note.
391 updated_on_will_change!
391 updated_on_will_change!
392 @current_journal
392 @current_journal
393 end
393 end
394
394
395 # Return true if the issue is closed, otherwise false
395 # Return true if the issue is closed, otherwise false
396 def closed?
396 def closed?
397 self.status.is_closed?
397 self.status.is_closed?
398 end
398 end
399
399
400 # Return true if the issue is being reopened
400 # Return true if the issue is being reopened
401 def reopened?
401 def reopened?
402 if !new_record? && status_id_changed?
402 if !new_record? && status_id_changed?
403 status_was = IssueStatus.find_by_id(status_id_was)
403 status_was = IssueStatus.find_by_id(status_id_was)
404 status_new = IssueStatus.find_by_id(status_id)
404 status_new = IssueStatus.find_by_id(status_id)
405 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
405 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
406 return true
406 return true
407 end
407 end
408 end
408 end
409 false
409 false
410 end
410 end
411
411
412 # Return true if the issue is being closed
412 # Return true if the issue is being closed
413 def closing?
413 def closing?
414 if !new_record? && status_id_changed?
414 if !new_record? && status_id_changed?
415 status_was = IssueStatus.find_by_id(status_id_was)
415 status_was = IssueStatus.find_by_id(status_id_was)
416 status_new = IssueStatus.find_by_id(status_id)
416 status_new = IssueStatus.find_by_id(status_id)
417 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
417 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
418 return true
418 return true
419 end
419 end
420 end
420 end
421 false
421 false
422 end
422 end
423
423
424 # Returns true if the issue is overdue
424 # Returns true if the issue is overdue
425 def overdue?
425 def overdue?
426 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
426 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
427 end
427 end
428
428
429 # Is the amount of work done less than it should for the due date
429 # Is the amount of work done less than it should for the due date
430 def behind_schedule?
430 def behind_schedule?
431 return false if start_date.nil? || due_date.nil?
431 return false if start_date.nil? || due_date.nil?
432 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
432 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
433 return done_date <= Date.today
433 return done_date <= Date.today
434 end
434 end
435
435
436 # Does this issue have children?
436 # Does this issue have children?
437 def children?
437 def children?
438 !leaf?
438 !leaf?
439 end
439 end
440
440
441 # Users the issue can be assigned to
441 # Users the issue can be assigned to
442 def assignable_users
442 def assignable_users
443 users = project.assignable_users
443 users = project.assignable_users
444 users << author if author
444 users << author if author
445 users.uniq.sort
445 users.uniq.sort
446 end
446 end
447
447
448 # Versions that the issue can be assigned to
448 # Versions that the issue can be assigned to
449 def assignable_versions
449 def assignable_versions
450 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
450 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
451 end
451 end
452
452
453 # Returns true if this issue is blocked by another issue that is still open
453 # Returns true if this issue is blocked by another issue that is still open
454 def blocked?
454 def blocked?
455 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
455 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
456 end
456 end
457
457
458 # Returns an array of status that user is able to apply
458 # Returns an array of status that user is able to apply
459 def new_statuses_allowed_to(user, include_default=false)
459 def new_statuses_allowed_to(user, include_default=false)
460 statuses = status.find_new_statuses_allowed_to(
460 statuses = status.find_new_statuses_allowed_to(
461 user.roles_for_project(project),
461 user.roles_for_project(project),
462 tracker,
462 tracker,
463 author == user,
463 author == user,
464 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
464 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
465 )
465 )
466 statuses << status unless statuses.empty?
466 statuses << status unless statuses.empty?
467 statuses << IssueStatus.default if include_default
467 statuses << IssueStatus.default if include_default
468 statuses = statuses.uniq.sort
468 statuses = statuses.uniq.sort
469 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
469 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
470 end
470 end
471
471
472 # Returns the mail adresses of users that should be notified
472 # Returns the mail adresses of users that should be notified
473 def recipients
473 def recipients
474 notified = project.notified_users
474 notified = project.notified_users
475 # Author and assignee are always notified unless they have been
475 # Author and assignee are always notified unless they have been
476 # locked or don't want to be notified
476 # locked or don't want to be notified
477 notified << author if author && author.active? && author.notify_about?(self)
477 notified << author if author && author.active? && author.notify_about?(self)
478 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
478 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
479 notified.uniq!
479 notified.uniq!
480 # Remove users that can not view the issue
480 # Remove users that can not view the issue
481 notified.reject! {|user| !visible?(user)}
481 notified.reject! {|user| !visible?(user)}
482 notified.collect(&:mail)
482 notified.collect(&:mail)
483 end
483 end
484
484
485 # Returns the total number of hours spent on this issue and its descendants
485 # Returns the total number of hours spent on this issue and its descendants
486 #
486 #
487 # Example:
487 # Example:
488 # spent_hours => 0.0
488 # spent_hours => 0.0
489 # spent_hours => 50.2
489 # spent_hours => 50.2
490 def spent_hours
490 def spent_hours
491 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
491 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
492 end
492 end
493
493
494 def relations
494 def relations
495 (relations_from + relations_to).sort
495 (relations_from + relations_to).sort
496 end
496 end
497
497
498 def all_dependent_issues(except=[])
498 def all_dependent_issues(except=[])
499 except << self
499 except << self
500 dependencies = []
500 dependencies = []
501 relations_from.each do |relation|
501 relations_from.each do |relation|
502 if relation.issue_to && !except.include?(relation.issue_to)
502 if relation.issue_to && !except.include?(relation.issue_to)
503 dependencies << relation.issue_to
503 dependencies << relation.issue_to
504 dependencies += relation.issue_to.all_dependent_issues(except)
504 dependencies += relation.issue_to.all_dependent_issues(except)
505 end
505 end
506 end
506 end
507 dependencies
507 dependencies
508 end
508 end
509
509
510 # Returns an array of issues that duplicate this one
510 # Returns an array of issues that duplicate this one
511 def duplicates
511 def duplicates
512 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
512 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
513 end
513 end
514
514
515 # Returns the due date or the target due date if any
515 # Returns the due date or the target due date if any
516 # Used on gantt chart
516 # Used on gantt chart
517 def due_before
517 def due_before
518 due_date || (fixed_version ? fixed_version.effective_date : nil)
518 due_date || (fixed_version ? fixed_version.effective_date : nil)
519 end
519 end
520
520
521 # Returns the time scheduled for this issue.
521 # Returns the time scheduled for this issue.
522 #
522 #
523 # Example:
523 # Example:
524 # Start Date: 2/26/09, End Date: 3/04/09
524 # Start Date: 2/26/09, End Date: 3/04/09
525 # duration => 6
525 # duration => 6
526 def duration
526 def duration
527 (start_date && due_date) ? due_date - start_date : 0
527 (start_date && due_date) ? due_date - start_date : 0
528 end
528 end
529
529
530 def soonest_start
530 def soonest_start
531 @soonest_start ||= (
531 @soonest_start ||= (
532 relations_to.collect{|relation| relation.successor_soonest_start} +
532 relations_to.collect{|relation| relation.successor_soonest_start} +
533 ancestors.collect(&:soonest_start)
533 ancestors.collect(&:soonest_start)
534 ).compact.max
534 ).compact.max
535 end
535 end
536
536
537 def reschedule_after(date)
537 def reschedule_after(date)
538 return if date.nil?
538 return if date.nil?
539 if leaf?
539 if leaf?
540 if start_date.nil? || start_date < date
540 if start_date.nil? || start_date < date
541 self.start_date, self.due_date = date, date + duration
541 self.start_date, self.due_date = date, date + duration
542 save
542 save
543 end
543 end
544 else
544 else
545 leaves.each do |leaf|
545 leaves.each do |leaf|
546 leaf.reschedule_after(date)
546 leaf.reschedule_after(date)
547 end
547 end
548 end
548 end
549 end
549 end
550
550
551 def <=>(issue)
551 def <=>(issue)
552 if issue.nil?
552 if issue.nil?
553 -1
553 -1
554 elsif root_id != issue.root_id
554 elsif root_id != issue.root_id
555 (root_id || 0) <=> (issue.root_id || 0)
555 (root_id || 0) <=> (issue.root_id || 0)
556 else
556 else
557 (lft || 0) <=> (issue.lft || 0)
557 (lft || 0) <=> (issue.lft || 0)
558 end
558 end
559 end
559 end
560
560
561 def to_s
561 def to_s
562 "#{tracker} ##{id}: #{subject}"
562 "#{tracker} ##{id}: #{subject}"
563 end
563 end
564
564
565 # Returns a string of css classes that apply to the issue
565 # Returns a string of css classes that apply to the issue
566 def css_classes
566 def css_classes
567 s = "issue status-#{status.position} priority-#{priority.position}"
567 s = "issue status-#{status.position} priority-#{priority.position}"
568 s << ' closed' if closed?
568 s << ' closed' if closed?
569 s << ' overdue' if overdue?
569 s << ' overdue' if overdue?
570 s << ' child' if child?
570 s << ' child' if child?
571 s << ' parent' unless leaf?
571 s << ' parent' unless leaf?
572 s << ' private' if is_private?
572 s << ' private' if is_private?
573 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
573 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
574 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
574 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
575 s
575 s
576 end
576 end
577
577
578 # Saves an issue, time_entry, attachments, and a journal from the parameters
578 # Saves an issue, time_entry, attachments, and a journal from the parameters
579 # Returns false if save fails
579 # Returns false if save fails
580 def save_issue_with_child_records(params, existing_time_entry=nil)
580 def save_issue_with_child_records(params, existing_time_entry=nil)
581 Issue.transaction do
581 Issue.transaction do
582 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
582 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
583 @time_entry = existing_time_entry || TimeEntry.new
583 @time_entry = existing_time_entry || TimeEntry.new
584 @time_entry.project = project
584 @time_entry.project = project
585 @time_entry.issue = self
585 @time_entry.issue = self
586 @time_entry.user = User.current
586 @time_entry.user = User.current
587 @time_entry.spent_on = Date.today
587 @time_entry.spent_on = Date.today
588 @time_entry.attributes = params[:time_entry]
588 @time_entry.attributes = params[:time_entry]
589 self.time_entries << @time_entry
589 self.time_entries << @time_entry
590 end
590 end
591
591
592 if valid?
592 if valid?
593 attachments = Attachment.attach_files(self, params[:attachments])
593 attachments = Attachment.attach_files(self, params[:attachments])
594
594
595 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
595 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
596 # TODO: Rename hook
596 # TODO: Rename hook
597 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
597 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
598 begin
598 begin
599 if save
599 if save
600 # TODO: Rename hook
600 # TODO: Rename hook
601 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
601 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
602 else
602 else
603 raise ActiveRecord::Rollback
603 raise ActiveRecord::Rollback
604 end
604 end
605 rescue ActiveRecord::StaleObjectError
605 rescue ActiveRecord::StaleObjectError
606 attachments[:files].each(&:destroy)
606 attachments[:files].each(&:destroy)
607 errors.add_to_base l(:notice_locking_conflict)
607 errors.add_to_base l(:notice_locking_conflict)
608 raise ActiveRecord::Rollback
608 raise ActiveRecord::Rollback
609 end
609 end
610 end
610 end
611 end
611 end
612 end
612 end
613
613
614 # Unassigns issues from +version+ if it's no longer shared with issue's project
614 # Unassigns issues from +version+ if it's no longer shared with issue's project
615 def self.update_versions_from_sharing_change(version)
615 def self.update_versions_from_sharing_change(version)
616 # Update issues assigned to the version
616 # Update issues assigned to the version
617 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
617 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
618 end
618 end
619
619
620 # Unassigns issues from versions that are no longer shared
620 # Unassigns issues from versions that are no longer shared
621 # after +project+ was moved
621 # after +project+ was moved
622 def self.update_versions_from_hierarchy_change(project)
622 def self.update_versions_from_hierarchy_change(project)
623 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
623 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
624 # Update issues of the moved projects and issues assigned to a version of a moved project
624 # Update issues of the moved projects and issues assigned to a version of a moved project
625 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
625 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
626 end
626 end
627
627
628 def parent_issue_id=(arg)
628 def parent_issue_id=(arg)
629 parent_issue_id = arg.blank? ? nil : arg.to_i
629 parent_issue_id = arg.blank? ? nil : arg.to_i
630 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
630 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
631 @parent_issue.id
631 @parent_issue.id
632 else
632 else
633 @parent_issue = nil
633 @parent_issue = nil
634 nil
634 nil
635 end
635 end
636 end
636 end
637
637
638 def parent_issue_id
638 def parent_issue_id
639 if instance_variable_defined? :@parent_issue
639 if instance_variable_defined? :@parent_issue
640 @parent_issue.nil? ? nil : @parent_issue.id
640 @parent_issue.nil? ? nil : @parent_issue.id
641 else
641 else
642 parent_id
642 parent_id
643 end
643 end
644 end
644 end
645
645
646 # Extracted from the ReportsController.
646 # Extracted from the ReportsController.
647 def self.by_tracker(project)
647 def self.by_tracker(project)
648 count_and_group_by(:project => project,
648 count_and_group_by(:project => project,
649 :field => 'tracker_id',
649 :field => 'tracker_id',
650 :joins => Tracker.table_name)
650 :joins => Tracker.table_name)
651 end
651 end
652
652
653 def self.by_version(project)
653 def self.by_version(project)
654 count_and_group_by(:project => project,
654 count_and_group_by(:project => project,
655 :field => 'fixed_version_id',
655 :field => 'fixed_version_id',
656 :joins => Version.table_name)
656 :joins => Version.table_name)
657 end
657 end
658
658
659 def self.by_priority(project)
659 def self.by_priority(project)
660 count_and_group_by(:project => project,
660 count_and_group_by(:project => project,
661 :field => 'priority_id',
661 :field => 'priority_id',
662 :joins => IssuePriority.table_name)
662 :joins => IssuePriority.table_name)
663 end
663 end
664
664
665 def self.by_category(project)
665 def self.by_category(project)
666 count_and_group_by(:project => project,
666 count_and_group_by(:project => project,
667 :field => 'category_id',
667 :field => 'category_id',
668 :joins => IssueCategory.table_name)
668 :joins => IssueCategory.table_name)
669 end
669 end
670
670
671 def self.by_assigned_to(project)
671 def self.by_assigned_to(project)
672 count_and_group_by(:project => project,
672 count_and_group_by(:project => project,
673 :field => 'assigned_to_id',
673 :field => 'assigned_to_id',
674 :joins => User.table_name)
674 :joins => User.table_name)
675 end
675 end
676
676
677 def self.by_author(project)
677 def self.by_author(project)
678 count_and_group_by(:project => project,
678 count_and_group_by(:project => project,
679 :field => 'author_id',
679 :field => 'author_id',
680 :joins => User.table_name)
680 :joins => User.table_name)
681 end
681 end
682
682
683 def self.by_subproject(project)
683 def self.by_subproject(project)
684 ActiveRecord::Base.connection.select_all("select s.id as status_id,
684 ActiveRecord::Base.connection.select_all("select s.id as status_id,
685 s.is_closed as closed,
685 s.is_closed as closed,
686 #{Issue.table_name}.project_id as project_id,
686 #{Issue.table_name}.project_id as project_id,
687 count(#{Issue.table_name}.id) as total
687 count(#{Issue.table_name}.id) as total
688 from
688 from
689 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
689 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
690 where
690 where
691 #{Issue.table_name}.status_id=s.id
691 #{Issue.table_name}.status_id=s.id
692 and #{Issue.table_name}.project_id = #{Project.table_name}.id
692 and #{Issue.table_name}.project_id = #{Project.table_name}.id
693 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
693 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
694 and #{Issue.table_name}.project_id <> #{project.id}
694 and #{Issue.table_name}.project_id <> #{project.id}
695 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
695 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
696 end
696 end
697 # End ReportsController extraction
697 # End ReportsController extraction
698
698
699 # Returns an array of projects that current user can move issues to
699 # Returns an array of projects that current user can move issues to
700 def self.allowed_target_projects_on_move
700 def self.allowed_target_projects_on_move
701 projects = []
701 projects = []
702 if User.current.admin?
702 if User.current.admin?
703 # admin is allowed to move issues to any active (visible) project
703 # admin is allowed to move issues to any active (visible) project
704 projects = Project.visible.all
704 projects = Project.visible.all
705 elsif User.current.logged?
705 elsif User.current.logged?
706 if Role.non_member.allowed_to?(:move_issues)
706 if Role.non_member.allowed_to?(:move_issues)
707 projects = Project.visible.all
707 projects = Project.visible.all
708 else
708 else
709 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
709 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
710 end
710 end
711 end
711 end
712 projects
712 projects
713 end
713 end
714
714
715 private
715 private
716
716
717 def update_nested_set_attributes
717 def update_nested_set_attributes
718 if root_id.nil?
718 if root_id.nil?
719 # issue was just created
719 # issue was just created
720 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
720 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
721 set_default_left_and_right
721 set_default_left_and_right
722 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
722 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
723 if @parent_issue
723 if @parent_issue
724 move_to_child_of(@parent_issue)
724 move_to_child_of(@parent_issue)
725 end
725 end
726 reload
726 reload
727 elsif parent_issue_id != parent_id
727 elsif parent_issue_id != parent_id
728 former_parent_id = parent_id
728 former_parent_id = parent_id
729 # moving an existing issue
729 # moving an existing issue
730 if @parent_issue && @parent_issue.root_id == root_id
730 if @parent_issue && @parent_issue.root_id == root_id
731 # inside the same tree
731 # inside the same tree
732 move_to_child_of(@parent_issue)
732 move_to_child_of(@parent_issue)
733 else
733 else
734 # to another tree
734 # to another tree
735 unless root?
735 unless root?
736 move_to_right_of(root)
736 move_to_right_of(root)
737 reload
737 reload
738 end
738 end
739 old_root_id = root_id
739 old_root_id = root_id
740 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
740 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
741 target_maxright = nested_set_scope.maximum(right_column_name) || 0
741 target_maxright = nested_set_scope.maximum(right_column_name) || 0
742 offset = target_maxright + 1 - lft
742 offset = target_maxright + 1 - lft
743 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
743 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
744 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
744 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
745 self[left_column_name] = lft + offset
745 self[left_column_name] = lft + offset
746 self[right_column_name] = rgt + offset
746 self[right_column_name] = rgt + offset
747 if @parent_issue
747 if @parent_issue
748 move_to_child_of(@parent_issue)
748 move_to_child_of(@parent_issue)
749 end
749 end
750 end
750 end
751 reload
751 reload
752 # delete invalid relations of all descendants
752 # delete invalid relations of all descendants
753 self_and_descendants.each do |issue|
753 self_and_descendants.each do |issue|
754 issue.relations.each do |relation|
754 issue.relations.each do |relation|
755 relation.destroy unless relation.valid?
755 relation.destroy unless relation.valid?
756 end
756 end
757 end
757 end
758 # update former parent
758 # update former parent
759 recalculate_attributes_for(former_parent_id) if former_parent_id
759 recalculate_attributes_for(former_parent_id) if former_parent_id
760 end
760 end
761 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
761 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
762 end
762 end
763
763
764 def update_parent_attributes
764 def update_parent_attributes
765 recalculate_attributes_for(parent_id) if parent_id
765 recalculate_attributes_for(parent_id) if parent_id
766 end
766 end
767
767
768 def recalculate_attributes_for(issue_id)
768 def recalculate_attributes_for(issue_id)
769 if issue_id && p = Issue.find_by_id(issue_id)
769 if issue_id && p = Issue.find_by_id(issue_id)
770 # priority = highest priority of children
770 # priority = highest priority of children
771 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
771 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
772 p.priority = IssuePriority.find_by_position(priority_position)
772 p.priority = IssuePriority.find_by_position(priority_position)
773 end
773 end
774
774
775 # start/due dates = lowest/highest dates of children
775 # start/due dates = lowest/highest dates of children
776 p.start_date = p.children.minimum(:start_date)
776 p.start_date = p.children.minimum(:start_date)
777 p.due_date = p.children.maximum(:due_date)
777 p.due_date = p.children.maximum(:due_date)
778 if p.start_date && p.due_date && p.due_date < p.start_date
778 if p.start_date && p.due_date && p.due_date < p.start_date
779 p.start_date, p.due_date = p.due_date, p.start_date
779 p.start_date, p.due_date = p.due_date, p.start_date
780 end
780 end
781
781
782 # done ratio = weighted average ratio of leaves
782 # done ratio = weighted average ratio of leaves
783 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
783 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
784 leaves_count = p.leaves.count
784 leaves_count = p.leaves.count
785 if leaves_count > 0
785 if leaves_count > 0
786 average = p.leaves.average(:estimated_hours).to_f
786 average = p.leaves.average(:estimated_hours).to_f
787 if average == 0
787 if average == 0
788 average = 1
788 average = 1
789 end
789 end
790 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
790 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
791 progress = done / (average * leaves_count)
791 progress = done / (average * leaves_count)
792 p.done_ratio = progress.round
792 p.done_ratio = progress.round
793 end
793 end
794 end
794 end
795
795
796 # estimate = sum of leaves estimates
796 # estimate = sum of leaves estimates
797 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
797 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
798 p.estimated_hours = nil if p.estimated_hours == 0.0
798 p.estimated_hours = nil if p.estimated_hours == 0.0
799
799
800 # ancestors will be recursively updated
800 # ancestors will be recursively updated
801 p.save(false)
801 p.save(false)
802 end
802 end
803 end
803 end
804
804
805 # Update issues so their versions are not pointing to a
805 # Update issues so their versions are not pointing to a
806 # fixed_version that is not shared with the issue's project
806 # fixed_version that is not shared with the issue's project
807 def self.update_versions(conditions=nil)
807 def self.update_versions(conditions=nil)
808 # Only need to update issues with a fixed_version from
808 # Only need to update issues with a fixed_version from
809 # a different project and that is not systemwide shared
809 # a different project and that is not systemwide shared
810 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
810 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
811 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
811 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
812 " AND #{Version.table_name}.sharing <> 'system'",
812 " AND #{Version.table_name}.sharing <> 'system'",
813 conditions),
813 conditions),
814 :include => [:project, :fixed_version]
814 :include => [:project, :fixed_version]
815 ).each do |issue|
815 ).each do |issue|
816 next if issue.project.nil? || issue.fixed_version.nil?
816 next if issue.project.nil? || issue.fixed_version.nil?
817 unless issue.project.shared_versions.include?(issue.fixed_version)
817 unless issue.project.shared_versions.include?(issue.fixed_version)
818 issue.init_journal(User.current)
818 issue.init_journal(User.current)
819 issue.fixed_version = nil
819 issue.fixed_version = nil
820 issue.save
820 issue.save
821 end
821 end
822 end
822 end
823 end
823 end
824
824
825 # Callback on attachment deletion
825 # Callback on attachment deletion
826 def attachment_removed(obj)
826 def attachment_removed(obj)
827 journal = init_journal(User.current)
827 journal = init_journal(User.current)
828 journal.details << JournalDetail.new(:property => 'attachment',
828 journal.details << JournalDetail.new(:property => 'attachment',
829 :prop_key => obj.id,
829 :prop_key => obj.id,
830 :old_value => obj.filename)
830 :old_value => obj.filename)
831 journal.save
831 journal.save
832 end
832 end
833
833
834 # Default assignment based on category
834 # Default assignment based on category
835 def default_assign
835 def default_assign
836 if assigned_to.nil? && category && category.assigned_to
836 if assigned_to.nil? && category && category.assigned_to
837 self.assigned_to = category.assigned_to
837 self.assigned_to = category.assigned_to
838 end
838 end
839 end
839 end
840
840
841 # Updates start/due dates of following issues
841 # Updates start/due dates of following issues
842 def reschedule_following_issues
842 def reschedule_following_issues
843 if start_date_changed? || due_date_changed?
843 if start_date_changed? || due_date_changed?
844 relations_from.each do |relation|
844 relations_from.each do |relation|
845 relation.set_issue_to_dates
845 relation.set_issue_to_dates
846 end
846 end
847 end
847 end
848 end
848 end
849
849
850 # Closes duplicates if the issue is being closed
850 # Closes duplicates if the issue is being closed
851 def close_duplicates
851 def close_duplicates
852 if closing?
852 if closing?
853 duplicates.each do |duplicate|
853 duplicates.each do |duplicate|
854 # Reload is need in case the duplicate was updated by a previous duplicate
854 # Reload is need in case the duplicate was updated by a previous duplicate
855 duplicate.reload
855 duplicate.reload
856 # Don't re-close it if it's already closed
856 # Don't re-close it if it's already closed
857 next if duplicate.closed?
857 next if duplicate.closed?
858 # Same user and notes
858 # Same user and notes
859 if @current_journal
859 if @current_journal
860 duplicate.init_journal(@current_journal.user, @current_journal.notes)
860 duplicate.init_journal(@current_journal.user, @current_journal.notes)
861 end
861 end
862 duplicate.update_attribute :status, self.status
862 duplicate.update_attribute :status, self.status
863 end
863 end
864 end
864 end
865 end
865 end
866
866
867 # Saves the changes in a Journal
867 # Saves the changes in a Journal
868 # Called after_save
868 # Called after_save
869 def create_journal
869 def create_journal
870 if @current_journal
870 if @current_journal
871 # attributes changes
871 # attributes changes
872 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
872 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
873 @current_journal.details << JournalDetail.new(:property => 'attr',
873 @current_journal.details << JournalDetail.new(:property => 'attr',
874 :prop_key => c,
874 :prop_key => c,
875 :old_value => @issue_before_change.send(c),
875 :old_value => @issue_before_change.send(c),
876 :value => send(c)) unless send(c)==@issue_before_change.send(c)
876 :value => send(c)) unless send(c)==@issue_before_change.send(c)
877 }
877 }
878 # custom fields changes
878 # custom fields changes
879 custom_values.each {|c|
879 custom_values.each {|c|
880 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
880 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
881 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
881 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
882 @current_journal.details << JournalDetail.new(:property => 'cf',
882 @current_journal.details << JournalDetail.new(:property => 'cf',
883 :prop_key => c.custom_field_id,
883 :prop_key => c.custom_field_id,
884 :old_value => @custom_values_before_change[c.custom_field_id],
884 :old_value => @custom_values_before_change[c.custom_field_id],
885 :value => c.value)
885 :value => c.value)
886 }
886 }
887 @current_journal.save
887 @current_journal.save
888 # reset current journal
888 # reset current journal
889 init_journal @current_journal.user, @current_journal.notes
889 init_journal @current_journal.user, @current_journal.notes
890 end
890 end
891 end
891 end
892
892
893 # Query generator for selecting groups of issue counts for a project
893 # Query generator for selecting groups of issue counts for a project
894 # based on specific criteria
894 # based on specific criteria
895 #
895 #
896 # Options
896 # Options
897 # * project - Project to search in.
897 # * project - Project to search in.
898 # * field - String. Issue field to key off of in the grouping.
898 # * field - String. Issue field to key off of in the grouping.
899 # * joins - String. The table name to join against.
899 # * joins - String. The table name to join against.
900 def self.count_and_group_by(options)
900 def self.count_and_group_by(options)
901 project = options.delete(:project)
901 project = options.delete(:project)
902 select_field = options.delete(:field)
902 select_field = options.delete(:field)
903 joins = options.delete(:joins)
903 joins = options.delete(:joins)
904
904
905 where = "#{Issue.table_name}.#{select_field}=j.id"
905 where = "#{Issue.table_name}.#{select_field}=j.id"
906
906
907 ActiveRecord::Base.connection.select_all("select s.id as status_id,
907 ActiveRecord::Base.connection.select_all("select s.id as status_id,
908 s.is_closed as closed,
908 s.is_closed as closed,
909 j.id as #{select_field},
909 j.id as #{select_field},
910 count(#{Issue.table_name}.id) as total
910 count(#{Issue.table_name}.id) as total
911 from
911 from
912 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
912 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
913 where
913 where
914 #{Issue.table_name}.status_id=s.id
914 #{Issue.table_name}.status_id=s.id
915 and #{where}
915 and #{where}
916 and #{Issue.table_name}.project_id=#{Project.table_name}.id
916 and #{Issue.table_name}.project_id=#{Project.table_name}.id
917 and #{visible_condition(User.current, :project => project)}
917 and #{visible_condition(User.current, :project => project)}
918 group by s.id, s.is_closed, j.id")
918 group by s.id, s.is_closed, j.id")
919 end
919 end
920 end
920 end
General Comments 0
You need to be logged in to leave comments. Login now