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