##// END OF EJS Templates
Merged r4263 from trunk....
Eric Davis -
r4209:2b69a96d6db8
parent child
Show More
@@ -1,840 +1,840
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_nested_set :scope => 'root_id'
35 acts_as_nested_set :scope => 'root_id'
36 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_attachable :after_remove => :attachment_removed
37 acts_as_customizable
37 acts_as_customizable
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
40 :include => [:project, :journals],
40 :include => [:project, :journals],
41 # sort by id so that limited eager loading doesn't break with postgresql
41 # sort by id so that limited eager loading doesn't break with postgresql
42 :order_column => "#{table_name}.id"
42 :order_column => "#{table_name}.id"
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
46
46
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
48 :author_key => :author_id
48 :author_key => :author_id
49
49
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
51
51
52 attr_reader :current_journal
52 attr_reader :current_journal
53
53
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
55
55
56 validates_length_of :subject, :maximum => 255
56 validates_length_of :subject, :maximum => 255
57 validates_inclusion_of :done_ratio, :in => 0..100
57 validates_inclusion_of :done_ratio, :in => 0..100
58 validates_numericality_of :estimated_hours, :allow_nil => true
58 validates_numericality_of :estimated_hours, :allow_nil => true
59
59
60 named_scope :visible, lambda {|*args| { :include => :project,
60 named_scope :visible, lambda {|*args| { :include => :project,
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
62
62
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
64
64
65 named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
65 named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
69
69
70 before_create :default_assign
70 before_create :default_assign
71 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
71 before_save :close_duplicates, :update_done_ratio_from_issue_status
72 after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
72 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
73 after_destroy :destroy_children
73 after_destroy :destroy_children
74 after_destroy :update_parent_attributes
74 after_destroy :update_parent_attributes
75
75
76 # Returns true if usr or current user is allowed to view the issue
76 # Returns true if usr or current user is allowed to view the issue
77 def visible?(usr=nil)
77 def visible?(usr=nil)
78 (usr || User.current).allowed_to?(:view_issues, self.project)
78 (usr || User.current).allowed_to?(:view_issues, self.project)
79 end
79 end
80
80
81 def after_initialize
81 def after_initialize
82 if new_record?
82 if new_record?
83 # set default values for new records only
83 # set default values for new records only
84 self.status ||= IssueStatus.default
84 self.status ||= IssueStatus.default
85 self.priority ||= IssuePriority.default
85 self.priority ||= IssuePriority.default
86 end
86 end
87 end
87 end
88
88
89 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
89 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
90 def available_custom_fields
90 def available_custom_fields
91 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
91 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
92 end
92 end
93
93
94 def copy_from(arg)
94 def copy_from(arg)
95 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
95 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
96 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
96 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
97 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
97 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
98 self.status = issue.status
98 self.status = issue.status
99 self
99 self
100 end
100 end
101
101
102 # Moves/copies an issue to a new project and tracker
102 # Moves/copies an issue to a new project and tracker
103 # Returns the moved/copied issue on success, false on failure
103 # Returns the moved/copied issue on success, false on failure
104 def move_to_project(*args)
104 def move_to_project(*args)
105 ret = Issue.transaction do
105 ret = Issue.transaction do
106 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
106 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
107 end || false
107 end || false
108 end
108 end
109
109
110 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
110 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
111 options ||= {}
111 options ||= {}
112 issue = options[:copy] ? self.class.new.copy_from(self) : self
112 issue = options[:copy] ? self.class.new.copy_from(self) : self
113
113
114 if new_project && issue.project_id != new_project.id
114 if new_project && issue.project_id != new_project.id
115 # delete issue relations
115 # delete issue relations
116 unless Setting.cross_project_issue_relations?
116 unless Setting.cross_project_issue_relations?
117 issue.relations_from.clear
117 issue.relations_from.clear
118 issue.relations_to.clear
118 issue.relations_to.clear
119 end
119 end
120 # issue is moved to another project
120 # issue is moved to another project
121 # reassign to the category with same name if any
121 # reassign to the category with same name if any
122 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
122 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
123 issue.category = new_category
123 issue.category = new_category
124 # Keep the fixed_version if it's still valid in the new_project
124 # Keep the fixed_version if it's still valid in the new_project
125 unless new_project.shared_versions.include?(issue.fixed_version)
125 unless new_project.shared_versions.include?(issue.fixed_version)
126 issue.fixed_version = nil
126 issue.fixed_version = nil
127 end
127 end
128 issue.project = new_project
128 issue.project = new_project
129 if issue.parent && issue.parent.project_id != issue.project_id
129 if issue.parent && issue.parent.project_id != issue.project_id
130 issue.parent_issue_id = nil
130 issue.parent_issue_id = nil
131 end
131 end
132 end
132 end
133 if new_tracker
133 if new_tracker
134 issue.tracker = new_tracker
134 issue.tracker = new_tracker
135 issue.reset_custom_values!
135 issue.reset_custom_values!
136 end
136 end
137 if options[:copy]
137 if options[:copy]
138 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
138 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
139 issue.status = if options[:attributes] && options[:attributes][:status_id]
139 issue.status = if options[:attributes] && options[:attributes][:status_id]
140 IssueStatus.find_by_id(options[:attributes][:status_id])
140 IssueStatus.find_by_id(options[:attributes][:status_id])
141 else
141 else
142 self.status
142 self.status
143 end
143 end
144 end
144 end
145 # Allow bulk setting of attributes on the issue
145 # Allow bulk setting of attributes on the issue
146 if options[:attributes]
146 if options[:attributes]
147 issue.attributes = options[:attributes]
147 issue.attributes = options[:attributes]
148 end
148 end
149 if issue.save
149 if issue.save
150 unless options[:copy]
150 unless options[:copy]
151 # Manually update project_id on related time entries
151 # Manually update project_id on related time entries
152 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
152 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
153
153
154 issue.children.each do |child|
154 issue.children.each do |child|
155 unless child.move_to_project_without_transaction(new_project)
155 unless child.move_to_project_without_transaction(new_project)
156 # Move failed and transaction was rollback'd
156 # Move failed and transaction was rollback'd
157 return false
157 return false
158 end
158 end
159 end
159 end
160 end
160 end
161 else
161 else
162 return false
162 return false
163 end
163 end
164 issue
164 issue
165 end
165 end
166
166
167 def status_id=(sid)
167 def status_id=(sid)
168 self.status = nil
168 self.status = nil
169 write_attribute(:status_id, sid)
169 write_attribute(:status_id, sid)
170 end
170 end
171
171
172 def priority_id=(pid)
172 def priority_id=(pid)
173 self.priority = nil
173 self.priority = nil
174 write_attribute(:priority_id, pid)
174 write_attribute(:priority_id, pid)
175 end
175 end
176
176
177 def tracker_id=(tid)
177 def tracker_id=(tid)
178 self.tracker = nil
178 self.tracker = nil
179 result = write_attribute(:tracker_id, tid)
179 result = write_attribute(:tracker_id, tid)
180 @custom_field_values = nil
180 @custom_field_values = nil
181 result
181 result
182 end
182 end
183
183
184 # Overrides attributes= so that tracker_id gets assigned first
184 # Overrides attributes= so that tracker_id gets assigned first
185 def attributes_with_tracker_first=(new_attributes, *args)
185 def attributes_with_tracker_first=(new_attributes, *args)
186 return if new_attributes.nil?
186 return if new_attributes.nil?
187 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
187 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
188 if new_tracker_id
188 if new_tracker_id
189 self.tracker_id = new_tracker_id
189 self.tracker_id = new_tracker_id
190 end
190 end
191 send :attributes_without_tracker_first=, new_attributes, *args
191 send :attributes_without_tracker_first=, new_attributes, *args
192 end
192 end
193 # Do not redefine alias chain on reload (see #4838)
193 # Do not redefine alias chain on reload (see #4838)
194 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
194 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
195
195
196 def estimated_hours=(h)
196 def estimated_hours=(h)
197 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
197 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
198 end
198 end
199
199
200 SAFE_ATTRIBUTES = %w(
200 SAFE_ATTRIBUTES = %w(
201 tracker_id
201 tracker_id
202 status_id
202 status_id
203 parent_issue_id
203 parent_issue_id
204 category_id
204 category_id
205 assigned_to_id
205 assigned_to_id
206 priority_id
206 priority_id
207 fixed_version_id
207 fixed_version_id
208 subject
208 subject
209 description
209 description
210 start_date
210 start_date
211 due_date
211 due_date
212 done_ratio
212 done_ratio
213 estimated_hours
213 estimated_hours
214 custom_field_values
214 custom_field_values
215 lock_version
215 lock_version
216 ) unless const_defined?(:SAFE_ATTRIBUTES)
216 ) unless const_defined?(:SAFE_ATTRIBUTES)
217
217
218 # Safely sets attributes
218 # Safely sets attributes
219 # Should be called from controllers instead of #attributes=
219 # Should be called from controllers instead of #attributes=
220 # attr_accessible is too rough because we still want things like
220 # attr_accessible is too rough because we still want things like
221 # Issue.new(:project => foo) to work
221 # Issue.new(:project => foo) to work
222 # TODO: move workflow/permission checks from controllers to here
222 # TODO: move workflow/permission checks from controllers to here
223 def safe_attributes=(attrs, user=User.current)
223 def safe_attributes=(attrs, user=User.current)
224 return if attrs.nil?
224 return if attrs.nil?
225 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
225 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
226 if attrs['status_id']
226 if attrs['status_id']
227 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
227 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
228 attrs.delete('status_id')
228 attrs.delete('status_id')
229 end
229 end
230 end
230 end
231
231
232 unless leaf?
232 unless leaf?
233 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
233 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
234 end
234 end
235
235
236 if attrs.has_key?('parent_issue_id')
236 if attrs.has_key?('parent_issue_id')
237 if !user.allowed_to?(:manage_subtasks, project)
237 if !user.allowed_to?(:manage_subtasks, project)
238 attrs.delete('parent_issue_id')
238 attrs.delete('parent_issue_id')
239 elsif !attrs['parent_issue_id'].blank?
239 elsif !attrs['parent_issue_id'].blank?
240 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
240 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
241 end
241 end
242 end
242 end
243
243
244 self.attributes = attrs
244 self.attributes = attrs
245 end
245 end
246
246
247 def done_ratio
247 def done_ratio
248 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
248 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
249 status.default_done_ratio
249 status.default_done_ratio
250 else
250 else
251 read_attribute(:done_ratio)
251 read_attribute(:done_ratio)
252 end
252 end
253 end
253 end
254
254
255 def self.use_status_for_done_ratio?
255 def self.use_status_for_done_ratio?
256 Setting.issue_done_ratio == 'issue_status'
256 Setting.issue_done_ratio == 'issue_status'
257 end
257 end
258
258
259 def self.use_field_for_done_ratio?
259 def self.use_field_for_done_ratio?
260 Setting.issue_done_ratio == 'issue_field'
260 Setting.issue_done_ratio == 'issue_field'
261 end
261 end
262
262
263 def validate
263 def validate
264 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
264 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
265 errors.add :due_date, :not_a_date
265 errors.add :due_date, :not_a_date
266 end
266 end
267
267
268 if self.due_date and self.start_date and self.due_date < self.start_date
268 if self.due_date and self.start_date and self.due_date < self.start_date
269 errors.add :due_date, :greater_than_start_date
269 errors.add :due_date, :greater_than_start_date
270 end
270 end
271
271
272 if start_date && soonest_start && start_date < soonest_start
272 if start_date && soonest_start && start_date < soonest_start
273 errors.add :start_date, :invalid
273 errors.add :start_date, :invalid
274 end
274 end
275
275
276 if fixed_version
276 if fixed_version
277 if !assignable_versions.include?(fixed_version)
277 if !assignable_versions.include?(fixed_version)
278 errors.add :fixed_version_id, :inclusion
278 errors.add :fixed_version_id, :inclusion
279 elsif reopened? && fixed_version.closed?
279 elsif reopened? && fixed_version.closed?
280 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
280 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
281 end
281 end
282 end
282 end
283
283
284 # Checks that the issue can not be added/moved to a disabled tracker
284 # Checks that the issue can not be added/moved to a disabled tracker
285 if project && (tracker_id_changed? || project_id_changed?)
285 if project && (tracker_id_changed? || project_id_changed?)
286 unless project.trackers.include?(tracker)
286 unless project.trackers.include?(tracker)
287 errors.add :tracker_id, :inclusion
287 errors.add :tracker_id, :inclusion
288 end
288 end
289 end
289 end
290
290
291 # Checks parent issue assignment
291 # Checks parent issue assignment
292 if @parent_issue
292 if @parent_issue
293 if @parent_issue.project_id != project_id
293 if @parent_issue.project_id != project_id
294 errors.add :parent_issue_id, :not_same_project
294 errors.add :parent_issue_id, :not_same_project
295 elsif !new_record?
295 elsif !new_record?
296 # moving an existing issue
296 # moving an existing issue
297 if @parent_issue.root_id != root_id
297 if @parent_issue.root_id != root_id
298 # we can always move to another tree
298 # we can always move to another tree
299 elsif move_possible?(@parent_issue)
299 elsif move_possible?(@parent_issue)
300 # move accepted inside tree
300 # move accepted inside tree
301 else
301 else
302 errors.add :parent_issue_id, :not_a_valid_parent
302 errors.add :parent_issue_id, :not_a_valid_parent
303 end
303 end
304 end
304 end
305 end
305 end
306 end
306 end
307
307
308 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
308 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
309 # even if the user turns off the setting later
309 # even if the user turns off the setting later
310 def update_done_ratio_from_issue_status
310 def update_done_ratio_from_issue_status
311 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
311 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
312 self.done_ratio = status.default_done_ratio
312 self.done_ratio = status.default_done_ratio
313 end
313 end
314 end
314 end
315
315
316 def init_journal(user, notes = "")
316 def init_journal(user, notes = "")
317 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
317 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
318 @issue_before_change = self.clone
318 @issue_before_change = self.clone
319 @issue_before_change.status = self.status
319 @issue_before_change.status = self.status
320 @custom_values_before_change = {}
320 @custom_values_before_change = {}
321 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
321 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
322 # Make sure updated_on is updated when adding a note.
322 # Make sure updated_on is updated when adding a note.
323 updated_on_will_change!
323 updated_on_will_change!
324 @current_journal
324 @current_journal
325 end
325 end
326
326
327 # Return true if the issue is closed, otherwise false
327 # Return true if the issue is closed, otherwise false
328 def closed?
328 def closed?
329 self.status.is_closed?
329 self.status.is_closed?
330 end
330 end
331
331
332 # Return true if the issue is being reopened
332 # Return true if the issue is being reopened
333 def reopened?
333 def reopened?
334 if !new_record? && status_id_changed?
334 if !new_record? && status_id_changed?
335 status_was = IssueStatus.find_by_id(status_id_was)
335 status_was = IssueStatus.find_by_id(status_id_was)
336 status_new = IssueStatus.find_by_id(status_id)
336 status_new = IssueStatus.find_by_id(status_id)
337 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
337 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
338 return true
338 return true
339 end
339 end
340 end
340 end
341 false
341 false
342 end
342 end
343
343
344 # Return true if the issue is being closed
344 # Return true if the issue is being closed
345 def closing?
345 def closing?
346 if !new_record? && status_id_changed?
346 if !new_record? && status_id_changed?
347 status_was = IssueStatus.find_by_id(status_id_was)
347 status_was = IssueStatus.find_by_id(status_id_was)
348 status_new = IssueStatus.find_by_id(status_id)
348 status_new = IssueStatus.find_by_id(status_id)
349 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
349 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
350 return true
350 return true
351 end
351 end
352 end
352 end
353 false
353 false
354 end
354 end
355
355
356 # Returns true if the issue is overdue
356 # Returns true if the issue is overdue
357 def overdue?
357 def overdue?
358 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
358 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
359 end
359 end
360
360
361 # Does this issue have children?
361 # Does this issue have children?
362 def children?
362 def children?
363 !leaf?
363 !leaf?
364 end
364 end
365
365
366 # Users the issue can be assigned to
366 # Users the issue can be assigned to
367 def assignable_users
367 def assignable_users
368 users = project.assignable_users
368 users = project.assignable_users
369 users << author if author
369 users << author if author
370 users.uniq.sort
370 users.uniq.sort
371 end
371 end
372
372
373 # Versions that the issue can be assigned to
373 # Versions that the issue can be assigned to
374 def assignable_versions
374 def assignable_versions
375 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
375 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
376 end
376 end
377
377
378 # Returns true if this issue is blocked by another issue that is still open
378 # Returns true if this issue is blocked by another issue that is still open
379 def blocked?
379 def blocked?
380 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
380 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
381 end
381 end
382
382
383 # Returns an array of status that user is able to apply
383 # Returns an array of status that user is able to apply
384 def new_statuses_allowed_to(user, include_default=false)
384 def new_statuses_allowed_to(user, include_default=false)
385 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
385 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
386 statuses << status unless statuses.empty?
386 statuses << status unless statuses.empty?
387 statuses << IssueStatus.default if include_default
387 statuses << IssueStatus.default if include_default
388 statuses = statuses.uniq.sort
388 statuses = statuses.uniq.sort
389 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
389 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
390 end
390 end
391
391
392 # Returns the mail adresses of users that should be notified
392 # Returns the mail adresses of users that should be notified
393 def recipients
393 def recipients
394 notified = project.notified_users
394 notified = project.notified_users
395 # Author and assignee are always notified unless they have been locked
395 # Author and assignee are always notified unless they have been locked
396 notified << author if author && author.active?
396 notified << author if author && author.active?
397 notified << assigned_to if assigned_to && assigned_to.active?
397 notified << assigned_to if assigned_to && assigned_to.active?
398 notified.uniq!
398 notified.uniq!
399 # Remove users that can not view the issue
399 # Remove users that can not view the issue
400 notified.reject! {|user| !visible?(user)}
400 notified.reject! {|user| !visible?(user)}
401 notified.collect(&:mail)
401 notified.collect(&:mail)
402 end
402 end
403
403
404 # Returns the total number of hours spent on this issue and its descendants
404 # Returns the total number of hours spent on this issue and its descendants
405 #
405 #
406 # Example:
406 # Example:
407 # spent_hours => 0.0
407 # spent_hours => 0.0
408 # spent_hours => 50.2
408 # spent_hours => 50.2
409 def spent_hours
409 def spent_hours
410 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
410 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
411 end
411 end
412
412
413 def relations
413 def relations
414 (relations_from + relations_to).sort
414 (relations_from + relations_to).sort
415 end
415 end
416
416
417 def all_dependent_issues
417 def all_dependent_issues
418 dependencies = []
418 dependencies = []
419 relations_from.each do |relation|
419 relations_from.each do |relation|
420 dependencies << relation.issue_to
420 dependencies << relation.issue_to
421 dependencies += relation.issue_to.all_dependent_issues
421 dependencies += relation.issue_to.all_dependent_issues
422 end
422 end
423 dependencies
423 dependencies
424 end
424 end
425
425
426 # Returns an array of issues that duplicate this one
426 # Returns an array of issues that duplicate this one
427 def duplicates
427 def duplicates
428 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
428 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
429 end
429 end
430
430
431 # Returns the due date or the target due date if any
431 # Returns the due date or the target due date if any
432 # Used on gantt chart
432 # Used on gantt chart
433 def due_before
433 def due_before
434 due_date || (fixed_version ? fixed_version.effective_date : nil)
434 due_date || (fixed_version ? fixed_version.effective_date : nil)
435 end
435 end
436
436
437 # Returns the time scheduled for this issue.
437 # Returns the time scheduled for this issue.
438 #
438 #
439 # Example:
439 # Example:
440 # Start Date: 2/26/09, End Date: 3/04/09
440 # Start Date: 2/26/09, End Date: 3/04/09
441 # duration => 6
441 # duration => 6
442 def duration
442 def duration
443 (start_date && due_date) ? due_date - start_date : 0
443 (start_date && due_date) ? due_date - start_date : 0
444 end
444 end
445
445
446 def soonest_start
446 def soonest_start
447 @soonest_start ||= (
447 @soonest_start ||= (
448 relations_to.collect{|relation| relation.successor_soonest_start} +
448 relations_to.collect{|relation| relation.successor_soonest_start} +
449 ancestors.collect(&:soonest_start)
449 ancestors.collect(&:soonest_start)
450 ).compact.max
450 ).compact.max
451 end
451 end
452
452
453 def reschedule_after(date)
453 def reschedule_after(date)
454 return if date.nil?
454 return if date.nil?
455 if leaf?
455 if leaf?
456 if start_date.nil? || start_date < date
456 if start_date.nil? || start_date < date
457 self.start_date, self.due_date = date, date + duration
457 self.start_date, self.due_date = date, date + duration
458 save
458 save
459 end
459 end
460 else
460 else
461 leaves.each do |leaf|
461 leaves.each do |leaf|
462 leaf.reschedule_after(date)
462 leaf.reschedule_after(date)
463 end
463 end
464 end
464 end
465 end
465 end
466
466
467 def <=>(issue)
467 def <=>(issue)
468 if issue.nil?
468 if issue.nil?
469 -1
469 -1
470 elsif root_id != issue.root_id
470 elsif root_id != issue.root_id
471 (root_id || 0) <=> (issue.root_id || 0)
471 (root_id || 0) <=> (issue.root_id || 0)
472 else
472 else
473 (lft || 0) <=> (issue.lft || 0)
473 (lft || 0) <=> (issue.lft || 0)
474 end
474 end
475 end
475 end
476
476
477 def to_s
477 def to_s
478 "#{tracker} ##{id}: #{subject}"
478 "#{tracker} ##{id}: #{subject}"
479 end
479 end
480
480
481 # Returns a string of css classes that apply to the issue
481 # Returns a string of css classes that apply to the issue
482 def css_classes
482 def css_classes
483 s = "issue status-#{status.position} priority-#{priority.position}"
483 s = "issue status-#{status.position} priority-#{priority.position}"
484 s << ' closed' if closed?
484 s << ' closed' if closed?
485 s << ' overdue' if overdue?
485 s << ' overdue' if overdue?
486 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
486 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
487 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
487 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
488 s
488 s
489 end
489 end
490
490
491 # Saves an issue, time_entry, attachments, and a journal from the parameters
491 # Saves an issue, time_entry, attachments, and a journal from the parameters
492 # Returns false if save fails
492 # Returns false if save fails
493 def save_issue_with_child_records(params, existing_time_entry=nil)
493 def save_issue_with_child_records(params, existing_time_entry=nil)
494 Issue.transaction do
494 Issue.transaction do
495 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
495 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
496 @time_entry = existing_time_entry || TimeEntry.new
496 @time_entry = existing_time_entry || TimeEntry.new
497 @time_entry.project = project
497 @time_entry.project = project
498 @time_entry.issue = self
498 @time_entry.issue = self
499 @time_entry.user = User.current
499 @time_entry.user = User.current
500 @time_entry.spent_on = Date.today
500 @time_entry.spent_on = Date.today
501 @time_entry.attributes = params[:time_entry]
501 @time_entry.attributes = params[:time_entry]
502 self.time_entries << @time_entry
502 self.time_entries << @time_entry
503 end
503 end
504
504
505 if valid?
505 if valid?
506 attachments = Attachment.attach_files(self, params[:attachments])
506 attachments = Attachment.attach_files(self, params[:attachments])
507
507
508 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
508 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
509 # TODO: Rename hook
509 # TODO: Rename hook
510 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
510 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
511 begin
511 begin
512 if save
512 if save
513 # TODO: Rename hook
513 # TODO: Rename hook
514 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
514 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
515 else
515 else
516 raise ActiveRecord::Rollback
516 raise ActiveRecord::Rollback
517 end
517 end
518 rescue ActiveRecord::StaleObjectError
518 rescue ActiveRecord::StaleObjectError
519 attachments[:files].each(&:destroy)
519 attachments[:files].each(&:destroy)
520 errors.add_to_base l(:notice_locking_conflict)
520 errors.add_to_base l(:notice_locking_conflict)
521 raise ActiveRecord::Rollback
521 raise ActiveRecord::Rollback
522 end
522 end
523 end
523 end
524 end
524 end
525 end
525 end
526
526
527 # Unassigns issues from +version+ if it's no longer shared with issue's project
527 # Unassigns issues from +version+ if it's no longer shared with issue's project
528 def self.update_versions_from_sharing_change(version)
528 def self.update_versions_from_sharing_change(version)
529 # Update issues assigned to the version
529 # Update issues assigned to the version
530 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
530 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
531 end
531 end
532
532
533 # Unassigns issues from versions that are no longer shared
533 # Unassigns issues from versions that are no longer shared
534 # after +project+ was moved
534 # after +project+ was moved
535 def self.update_versions_from_hierarchy_change(project)
535 def self.update_versions_from_hierarchy_change(project)
536 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
536 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
537 # Update issues of the moved projects and issues assigned to a version of a moved project
537 # Update issues of the moved projects and issues assigned to a version of a moved project
538 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
538 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
539 end
539 end
540
540
541 def parent_issue_id=(arg)
541 def parent_issue_id=(arg)
542 parent_issue_id = arg.blank? ? nil : arg.to_i
542 parent_issue_id = arg.blank? ? nil : arg.to_i
543 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
543 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
544 @parent_issue.id
544 @parent_issue.id
545 else
545 else
546 @parent_issue = nil
546 @parent_issue = nil
547 nil
547 nil
548 end
548 end
549 end
549 end
550
550
551 def parent_issue_id
551 def parent_issue_id
552 if instance_variable_defined? :@parent_issue
552 if instance_variable_defined? :@parent_issue
553 @parent_issue.nil? ? nil : @parent_issue.id
553 @parent_issue.nil? ? nil : @parent_issue.id
554 else
554 else
555 parent_id
555 parent_id
556 end
556 end
557 end
557 end
558
558
559 # Extracted from the ReportsController.
559 # Extracted from the ReportsController.
560 def self.by_tracker(project)
560 def self.by_tracker(project)
561 count_and_group_by(:project => project,
561 count_and_group_by(:project => project,
562 :field => 'tracker_id',
562 :field => 'tracker_id',
563 :joins => Tracker.table_name)
563 :joins => Tracker.table_name)
564 end
564 end
565
565
566 def self.by_version(project)
566 def self.by_version(project)
567 count_and_group_by(:project => project,
567 count_and_group_by(:project => project,
568 :field => 'fixed_version_id',
568 :field => 'fixed_version_id',
569 :joins => Version.table_name)
569 :joins => Version.table_name)
570 end
570 end
571
571
572 def self.by_priority(project)
572 def self.by_priority(project)
573 count_and_group_by(:project => project,
573 count_and_group_by(:project => project,
574 :field => 'priority_id',
574 :field => 'priority_id',
575 :joins => IssuePriority.table_name)
575 :joins => IssuePriority.table_name)
576 end
576 end
577
577
578 def self.by_category(project)
578 def self.by_category(project)
579 count_and_group_by(:project => project,
579 count_and_group_by(:project => project,
580 :field => 'category_id',
580 :field => 'category_id',
581 :joins => IssueCategory.table_name)
581 :joins => IssueCategory.table_name)
582 end
582 end
583
583
584 def self.by_assigned_to(project)
584 def self.by_assigned_to(project)
585 count_and_group_by(:project => project,
585 count_and_group_by(:project => project,
586 :field => 'assigned_to_id',
586 :field => 'assigned_to_id',
587 :joins => User.table_name)
587 :joins => User.table_name)
588 end
588 end
589
589
590 def self.by_author(project)
590 def self.by_author(project)
591 count_and_group_by(:project => project,
591 count_and_group_by(:project => project,
592 :field => 'author_id',
592 :field => 'author_id',
593 :joins => User.table_name)
593 :joins => User.table_name)
594 end
594 end
595
595
596 def self.by_subproject(project)
596 def self.by_subproject(project)
597 ActiveRecord::Base.connection.select_all("select s.id as status_id,
597 ActiveRecord::Base.connection.select_all("select s.id as status_id,
598 s.is_closed as closed,
598 s.is_closed as closed,
599 i.project_id as project_id,
599 i.project_id as project_id,
600 count(i.id) as total
600 count(i.id) as total
601 from
601 from
602 #{Issue.table_name} i, #{IssueStatus.table_name} s
602 #{Issue.table_name} i, #{IssueStatus.table_name} s
603 where
603 where
604 i.status_id=s.id
604 i.status_id=s.id
605 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
605 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
606 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
606 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
607 end
607 end
608 # End ReportsController extraction
608 # End ReportsController extraction
609
609
610 # Returns an array of projects that current user can move issues to
610 # Returns an array of projects that current user can move issues to
611 def self.allowed_target_projects_on_move
611 def self.allowed_target_projects_on_move
612 projects = []
612 projects = []
613 if User.current.admin?
613 if User.current.admin?
614 # admin is allowed to move issues to any active (visible) project
614 # admin is allowed to move issues to any active (visible) project
615 projects = Project.visible.all
615 projects = Project.visible.all
616 elsif User.current.logged?
616 elsif User.current.logged?
617 if Role.non_member.allowed_to?(:move_issues)
617 if Role.non_member.allowed_to?(:move_issues)
618 projects = Project.visible.all
618 projects = Project.visible.all
619 else
619 else
620 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
620 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
621 end
621 end
622 end
622 end
623 projects
623 projects
624 end
624 end
625
625
626 private
626 private
627
627
628 def update_nested_set_attributes
628 def update_nested_set_attributes
629 if root_id.nil?
629 if root_id.nil?
630 # issue was just created
630 # issue was just created
631 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
631 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
632 set_default_left_and_right
632 set_default_left_and_right
633 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
633 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
634 if @parent_issue
634 if @parent_issue
635 move_to_child_of(@parent_issue)
635 move_to_child_of(@parent_issue)
636 end
636 end
637 reload
637 reload
638 elsif parent_issue_id != parent_id
638 elsif parent_issue_id != parent_id
639 former_parent_id = parent_id
639 former_parent_id = parent_id
640 # moving an existing issue
640 # moving an existing issue
641 if @parent_issue && @parent_issue.root_id == root_id
641 if @parent_issue && @parent_issue.root_id == root_id
642 # inside the same tree
642 # inside the same tree
643 move_to_child_of(@parent_issue)
643 move_to_child_of(@parent_issue)
644 else
644 else
645 # to another tree
645 # to another tree
646 unless root?
646 unless root?
647 move_to_right_of(root)
647 move_to_right_of(root)
648 reload
648 reload
649 end
649 end
650 old_root_id = root_id
650 old_root_id = root_id
651 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
651 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
652 target_maxright = nested_set_scope.maximum(right_column_name) || 0
652 target_maxright = nested_set_scope.maximum(right_column_name) || 0
653 offset = target_maxright + 1 - lft
653 offset = target_maxright + 1 - lft
654 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
654 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
655 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
655 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
656 self[left_column_name] = lft + offset
656 self[left_column_name] = lft + offset
657 self[right_column_name] = rgt + offset
657 self[right_column_name] = rgt + offset
658 if @parent_issue
658 if @parent_issue
659 move_to_child_of(@parent_issue)
659 move_to_child_of(@parent_issue)
660 end
660 end
661 end
661 end
662 reload
662 reload
663 # delete invalid relations of all descendants
663 # delete invalid relations of all descendants
664 self_and_descendants.each do |issue|
664 self_and_descendants.each do |issue|
665 issue.relations.each do |relation|
665 issue.relations.each do |relation|
666 relation.destroy unless relation.valid?
666 relation.destroy unless relation.valid?
667 end
667 end
668 end
668 end
669 # update former parent
669 # update former parent
670 recalculate_attributes_for(former_parent_id) if former_parent_id
670 recalculate_attributes_for(former_parent_id) if former_parent_id
671 end
671 end
672 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
672 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
673 end
673 end
674
674
675 def update_parent_attributes
675 def update_parent_attributes
676 recalculate_attributes_for(parent_id) if parent_id
676 recalculate_attributes_for(parent_id) if parent_id
677 end
677 end
678
678
679 def recalculate_attributes_for(issue_id)
679 def recalculate_attributes_for(issue_id)
680 if issue_id && p = Issue.find_by_id(issue_id)
680 if issue_id && p = Issue.find_by_id(issue_id)
681 # priority = highest priority of children
681 # priority = highest priority of children
682 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
682 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
683 p.priority = IssuePriority.find_by_position(priority_position)
683 p.priority = IssuePriority.find_by_position(priority_position)
684 end
684 end
685
685
686 # start/due dates = lowest/highest dates of children
686 # start/due dates = lowest/highest dates of children
687 p.start_date = p.children.minimum(:start_date)
687 p.start_date = p.children.minimum(:start_date)
688 p.due_date = p.children.maximum(:due_date)
688 p.due_date = p.children.maximum(:due_date)
689 if p.start_date && p.due_date && p.due_date < p.start_date
689 if p.start_date && p.due_date && p.due_date < p.start_date
690 p.start_date, p.due_date = p.due_date, p.start_date
690 p.start_date, p.due_date = p.due_date, p.start_date
691 end
691 end
692
692
693 # done ratio = weighted average ratio of leaves
693 # done ratio = weighted average ratio of leaves
694 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
694 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
695 leaves_count = p.leaves.count
695 leaves_count = p.leaves.count
696 if leaves_count > 0
696 if leaves_count > 0
697 average = p.leaves.average(:estimated_hours).to_f
697 average = p.leaves.average(:estimated_hours).to_f
698 if average == 0
698 if average == 0
699 average = 1
699 average = 1
700 end
700 end
701 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
701 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
702 progress = done / (average * leaves_count)
702 progress = done / (average * leaves_count)
703 p.done_ratio = progress.round
703 p.done_ratio = progress.round
704 end
704 end
705 end
705 end
706
706
707 # estimate = sum of leaves estimates
707 # estimate = sum of leaves estimates
708 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
708 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
709 p.estimated_hours = nil if p.estimated_hours == 0.0
709 p.estimated_hours = nil if p.estimated_hours == 0.0
710
710
711 # ancestors will be recursively updated
711 # ancestors will be recursively updated
712 p.save(false)
712 p.save(false)
713 end
713 end
714 end
714 end
715
715
716 def destroy_children
716 def destroy_children
717 unless leaf?
717 unless leaf?
718 children.each do |child|
718 children.each do |child|
719 child.destroy
719 child.destroy
720 end
720 end
721 end
721 end
722 end
722 end
723
723
724 # Update issues so their versions are not pointing to a
724 # Update issues so their versions are not pointing to a
725 # fixed_version that is not shared with the issue's project
725 # fixed_version that is not shared with the issue's project
726 def self.update_versions(conditions=nil)
726 def self.update_versions(conditions=nil)
727 # Only need to update issues with a fixed_version from
727 # Only need to update issues with a fixed_version from
728 # a different project and that is not systemwide shared
728 # a different project and that is not systemwide shared
729 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
729 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
730 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
730 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
731 " AND #{Version.table_name}.sharing <> 'system'",
731 " AND #{Version.table_name}.sharing <> 'system'",
732 conditions),
732 conditions),
733 :include => [:project, :fixed_version]
733 :include => [:project, :fixed_version]
734 ).each do |issue|
734 ).each do |issue|
735 next if issue.project.nil? || issue.fixed_version.nil?
735 next if issue.project.nil? || issue.fixed_version.nil?
736 unless issue.project.shared_versions.include?(issue.fixed_version)
736 unless issue.project.shared_versions.include?(issue.fixed_version)
737 issue.init_journal(User.current)
737 issue.init_journal(User.current)
738 issue.fixed_version = nil
738 issue.fixed_version = nil
739 issue.save
739 issue.save
740 end
740 end
741 end
741 end
742 end
742 end
743
743
744 # Callback on attachment deletion
744 # Callback on attachment deletion
745 def attachment_removed(obj)
745 def attachment_removed(obj)
746 journal = init_journal(User.current)
746 journal = init_journal(User.current)
747 journal.details << JournalDetail.new(:property => 'attachment',
747 journal.details << JournalDetail.new(:property => 'attachment',
748 :prop_key => obj.id,
748 :prop_key => obj.id,
749 :old_value => obj.filename)
749 :old_value => obj.filename)
750 journal.save
750 journal.save
751 end
751 end
752
752
753 # Default assignment based on category
753 # Default assignment based on category
754 def default_assign
754 def default_assign
755 if assigned_to.nil? && category && category.assigned_to
755 if assigned_to.nil? && category && category.assigned_to
756 self.assigned_to = category.assigned_to
756 self.assigned_to = category.assigned_to
757 end
757 end
758 end
758 end
759
759
760 # Updates start/due dates of following issues
760 # Updates start/due dates of following issues
761 def reschedule_following_issues
761 def reschedule_following_issues
762 if start_date_changed? || due_date_changed?
762 if start_date_changed? || due_date_changed?
763 relations_from.each do |relation|
763 relations_from.each do |relation|
764 relation.set_issue_to_dates
764 relation.set_issue_to_dates
765 end
765 end
766 end
766 end
767 end
767 end
768
768
769 # Closes duplicates if the issue is being closed
769 # Closes duplicates if the issue is being closed
770 def close_duplicates
770 def close_duplicates
771 if closing?
771 if closing?
772 duplicates.each do |duplicate|
772 duplicates.each do |duplicate|
773 # Reload is need in case the duplicate was updated by a previous duplicate
773 # Reload is need in case the duplicate was updated by a previous duplicate
774 duplicate.reload
774 duplicate.reload
775 # Don't re-close it if it's already closed
775 # Don't re-close it if it's already closed
776 next if duplicate.closed?
776 next if duplicate.closed?
777 # Same user and notes
777 # Same user and notes
778 if @current_journal
778 if @current_journal
779 duplicate.init_journal(@current_journal.user, @current_journal.notes)
779 duplicate.init_journal(@current_journal.user, @current_journal.notes)
780 end
780 end
781 duplicate.update_attribute :status, self.status
781 duplicate.update_attribute :status, self.status
782 end
782 end
783 end
783 end
784 end
784 end
785
785
786 # Saves the changes in a Journal
786 # Saves the changes in a Journal
787 # Called after_save
787 # Called after_save
788 def create_journal
788 def create_journal
789 if @current_journal
789 if @current_journal
790 # attributes changes
790 # attributes changes
791 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
791 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
792 @current_journal.details << JournalDetail.new(:property => 'attr',
792 @current_journal.details << JournalDetail.new(:property => 'attr',
793 :prop_key => c,
793 :prop_key => c,
794 :old_value => @issue_before_change.send(c),
794 :old_value => @issue_before_change.send(c),
795 :value => send(c)) unless send(c)==@issue_before_change.send(c)
795 :value => send(c)) unless send(c)==@issue_before_change.send(c)
796 }
796 }
797 # custom fields changes
797 # custom fields changes
798 custom_values.each {|c|
798 custom_values.each {|c|
799 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
799 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
800 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
800 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
801 @current_journal.details << JournalDetail.new(:property => 'cf',
801 @current_journal.details << JournalDetail.new(:property => 'cf',
802 :prop_key => c.custom_field_id,
802 :prop_key => c.custom_field_id,
803 :old_value => @custom_values_before_change[c.custom_field_id],
803 :old_value => @custom_values_before_change[c.custom_field_id],
804 :value => c.value)
804 :value => c.value)
805 }
805 }
806 @current_journal.save
806 @current_journal.save
807 # reset current journal
807 # reset current journal
808 init_journal @current_journal.user, @current_journal.notes
808 init_journal @current_journal.user, @current_journal.notes
809 end
809 end
810 end
810 end
811
811
812 # Query generator for selecting groups of issue counts for a project
812 # Query generator for selecting groups of issue counts for a project
813 # based on specific criteria
813 # based on specific criteria
814 #
814 #
815 # Options
815 # Options
816 # * project - Project to search in.
816 # * project - Project to search in.
817 # * field - String. Issue field to key off of in the grouping.
817 # * field - String. Issue field to key off of in the grouping.
818 # * joins - String. The table name to join against.
818 # * joins - String. The table name to join against.
819 def self.count_and_group_by(options)
819 def self.count_and_group_by(options)
820 project = options.delete(:project)
820 project = options.delete(:project)
821 select_field = options.delete(:field)
821 select_field = options.delete(:field)
822 joins = options.delete(:joins)
822 joins = options.delete(:joins)
823
823
824 where = "i.#{select_field}=j.id"
824 where = "i.#{select_field}=j.id"
825
825
826 ActiveRecord::Base.connection.select_all("select s.id as status_id,
826 ActiveRecord::Base.connection.select_all("select s.id as status_id,
827 s.is_closed as closed,
827 s.is_closed as closed,
828 j.id as #{select_field},
828 j.id as #{select_field},
829 count(i.id) as total
829 count(i.id) as total
830 from
830 from
831 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
831 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
832 where
832 where
833 i.status_id=s.id
833 i.status_id=s.id
834 and #{where}
834 and #{where}
835 and i.project_id=#{project.id}
835 and i.project_id=#{project.id}
836 group by s.id, s.is_closed, j.id")
836 group by s.id, s.is_closed, j.id")
837 end
837 end
838
838
839
839
840 end
840 end
@@ -1,738 +1,749
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :enabled_modules,
23 :enabled_modules,
24 :versions,
24 :versions,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :enumerations,
26 :enumerations,
27 :issues,
27 :issues,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :time_entries
29 :time_entries
30
30
31 def test_create
31 def test_create
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
33 assert issue.save
33 assert issue.save
34 issue.reload
34 issue.reload
35 assert_equal 1.5, issue.estimated_hours
35 assert_equal 1.5, issue.estimated_hours
36 end
36 end
37
37
38 def test_create_minimal
38 def test_create_minimal
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 assert issue.save
40 assert issue.save
41 assert issue.description.nil?
41 assert issue.description.nil?
42 end
42 end
43
43
44 def test_create_with_required_custom_field
44 def test_create_with_required_custom_field
45 field = IssueCustomField.find_by_name('Database')
45 field = IssueCustomField.find_by_name('Database')
46 field.update_attribute(:is_required, true)
46 field.update_attribute(:is_required, true)
47
47
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
49 assert issue.available_custom_fields.include?(field)
49 assert issue.available_custom_fields.include?(field)
50 # No value for the custom field
50 # No value for the custom field
51 assert !issue.save
51 assert !issue.save
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 # Blank value
53 # Blank value
54 issue.custom_field_values = { field.id => '' }
54 issue.custom_field_values = { field.id => '' }
55 assert !issue.save
55 assert !issue.save
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 # Invalid value
57 # Invalid value
58 issue.custom_field_values = { field.id => 'SQLServer' }
58 issue.custom_field_values = { field.id => 'SQLServer' }
59 assert !issue.save
59 assert !issue.save
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 # Valid value
61 # Valid value
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 assert issue.save
63 assert issue.save
64 issue.reload
64 issue.reload
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 end
66 end
67
67
68 def test_visible_scope_for_anonymous
68 def test_visible_scope_for_anonymous
69 # Anonymous user should see issues of public projects only
69 # Anonymous user should see issues of public projects only
70 issues = Issue.visible(User.anonymous).all
70 issues = Issue.visible(User.anonymous).all
71 assert issues.any?
71 assert issues.any?
72 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 assert_nil issues.detect {|issue| !issue.project.is_public?}
73 # Anonymous user should not see issues without permission
73 # Anonymous user should not see issues without permission
74 Role.anonymous.remove_permission!(:view_issues)
74 Role.anonymous.remove_permission!(:view_issues)
75 issues = Issue.visible(User.anonymous).all
75 issues = Issue.visible(User.anonymous).all
76 assert issues.empty?
76 assert issues.empty?
77 end
77 end
78
78
79 def test_visible_scope_for_user
79 def test_visible_scope_for_user
80 user = User.find(9)
80 user = User.find(9)
81 assert user.projects.empty?
81 assert user.projects.empty?
82 # Non member user should see issues of public projects only
82 # Non member user should see issues of public projects only
83 issues = Issue.visible(user).all
83 issues = Issue.visible(user).all
84 assert issues.any?
84 assert issues.any?
85 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 assert_nil issues.detect {|issue| !issue.project.is_public?}
86 # Non member user should not see issues without permission
86 # Non member user should not see issues without permission
87 Role.non_member.remove_permission!(:view_issues)
87 Role.non_member.remove_permission!(:view_issues)
88 user.reload
88 user.reload
89 issues = Issue.visible(user).all
89 issues = Issue.visible(user).all
90 assert issues.empty?
90 assert issues.empty?
91 # User should see issues of projects for which he has view_issues permissions only
91 # User should see issues of projects for which he has view_issues permissions only
92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
93 user.reload
93 user.reload
94 issues = Issue.visible(user).all
94 issues = Issue.visible(user).all
95 assert issues.any?
95 assert issues.any?
96 assert_nil issues.detect {|issue| issue.project_id != 2}
96 assert_nil issues.detect {|issue| issue.project_id != 2}
97 end
97 end
98
98
99 def test_visible_scope_for_admin
99 def test_visible_scope_for_admin
100 user = User.find(1)
100 user = User.find(1)
101 user.members.each(&:destroy)
101 user.members.each(&:destroy)
102 assert user.projects.empty?
102 assert user.projects.empty?
103 issues = Issue.visible(user).all
103 issues = Issue.visible(user).all
104 assert issues.any?
104 assert issues.any?
105 # Admin should see issues on private projects that he does not belong to
105 # Admin should see issues on private projects that he does not belong to
106 assert issues.detect {|issue| !issue.project.is_public?}
106 assert issues.detect {|issue| !issue.project.is_public?}
107 end
107 end
108
108
109 def test_errors_full_messages_should_include_custom_fields_errors
109 def test_errors_full_messages_should_include_custom_fields_errors
110 field = IssueCustomField.find_by_name('Database')
110 field = IssueCustomField.find_by_name('Database')
111
111
112 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
112 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
113 assert issue.available_custom_fields.include?(field)
113 assert issue.available_custom_fields.include?(field)
114 # Invalid value
114 # Invalid value
115 issue.custom_field_values = { field.id => 'SQLServer' }
115 issue.custom_field_values = { field.id => 'SQLServer' }
116
116
117 assert !issue.valid?
117 assert !issue.valid?
118 assert_equal 1, issue.errors.full_messages.size
118 assert_equal 1, issue.errors.full_messages.size
119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
120 end
120 end
121
121
122 def test_update_issue_with_required_custom_field
122 def test_update_issue_with_required_custom_field
123 field = IssueCustomField.find_by_name('Database')
123 field = IssueCustomField.find_by_name('Database')
124 field.update_attribute(:is_required, true)
124 field.update_attribute(:is_required, true)
125
125
126 issue = Issue.find(1)
126 issue = Issue.find(1)
127 assert_nil issue.custom_value_for(field)
127 assert_nil issue.custom_value_for(field)
128 assert issue.available_custom_fields.include?(field)
128 assert issue.available_custom_fields.include?(field)
129 # No change to custom values, issue can be saved
129 # No change to custom values, issue can be saved
130 assert issue.save
130 assert issue.save
131 # Blank value
131 # Blank value
132 issue.custom_field_values = { field.id => '' }
132 issue.custom_field_values = { field.id => '' }
133 assert !issue.save
133 assert !issue.save
134 # Valid value
134 # Valid value
135 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 issue.custom_field_values = { field.id => 'PostgreSQL' }
136 assert issue.save
136 assert issue.save
137 issue.reload
137 issue.reload
138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
139 end
139 end
140
140
141 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 def test_should_not_update_attributes_if_custom_fields_validation_fails
142 issue = Issue.find(1)
142 issue = Issue.find(1)
143 field = IssueCustomField.find_by_name('Database')
143 field = IssueCustomField.find_by_name('Database')
144 assert issue.available_custom_fields.include?(field)
144 assert issue.available_custom_fields.include?(field)
145
145
146 issue.custom_field_values = { field.id => 'Invalid' }
146 issue.custom_field_values = { field.id => 'Invalid' }
147 issue.subject = 'Should be not be saved'
147 issue.subject = 'Should be not be saved'
148 assert !issue.save
148 assert !issue.save
149
149
150 issue.reload
150 issue.reload
151 assert_equal "Can't print recipes", issue.subject
151 assert_equal "Can't print recipes", issue.subject
152 end
152 end
153
153
154 def test_should_not_recreate_custom_values_objects_on_update
154 def test_should_not_recreate_custom_values_objects_on_update
155 field = IssueCustomField.find_by_name('Database')
155 field = IssueCustomField.find_by_name('Database')
156
156
157 issue = Issue.find(1)
157 issue = Issue.find(1)
158 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 assert issue.save
159 assert issue.save
160 custom_value = issue.custom_value_for(field)
160 custom_value = issue.custom_value_for(field)
161 issue.reload
161 issue.reload
162 issue.custom_field_values = { field.id => 'MySQL' }
162 issue.custom_field_values = { field.id => 'MySQL' }
163 assert issue.save
163 assert issue.save
164 issue.reload
164 issue.reload
165 assert_equal custom_value.id, issue.custom_value_for(field).id
165 assert_equal custom_value.id, issue.custom_value_for(field).id
166 end
166 end
167
167
168 def test_assigning_tracker_id_should_reload_custom_fields_values
168 def test_assigning_tracker_id_should_reload_custom_fields_values
169 issue = Issue.new(:project => Project.find(1))
169 issue = Issue.new(:project => Project.find(1))
170 assert issue.custom_field_values.empty?
170 assert issue.custom_field_values.empty?
171 issue.tracker_id = 1
171 issue.tracker_id = 1
172 assert issue.custom_field_values.any?
172 assert issue.custom_field_values.any?
173 end
173 end
174
174
175 def test_assigning_attributes_should_assign_tracker_id_first
175 def test_assigning_attributes_should_assign_tracker_id_first
176 attributes = ActiveSupport::OrderedHash.new
176 attributes = ActiveSupport::OrderedHash.new
177 attributes['custom_field_values'] = { '1' => 'MySQL' }
177 attributes['custom_field_values'] = { '1' => 'MySQL' }
178 attributes['tracker_id'] = '1'
178 attributes['tracker_id'] = '1'
179 issue = Issue.new(:project => Project.find(1))
179 issue = Issue.new(:project => Project.find(1))
180 issue.attributes = attributes
180 issue.attributes = attributes
181 assert_not_nil issue.custom_value_for(1)
181 assert_not_nil issue.custom_value_for(1)
182 assert_equal 'MySQL', issue.custom_value_for(1).value
182 assert_equal 'MySQL', issue.custom_value_for(1).value
183 end
183 end
184
184
185 def test_should_update_issue_with_disabled_tracker
185 def test_should_update_issue_with_disabled_tracker
186 p = Project.find(1)
186 p = Project.find(1)
187 issue = Issue.find(1)
187 issue = Issue.find(1)
188
188
189 p.trackers.delete(issue.tracker)
189 p.trackers.delete(issue.tracker)
190 assert !p.trackers.include?(issue.tracker)
190 assert !p.trackers.include?(issue.tracker)
191
191
192 issue.reload
192 issue.reload
193 issue.subject = 'New subject'
193 issue.subject = 'New subject'
194 assert issue.save
194 assert issue.save
195 end
195 end
196
196
197 def test_should_not_set_a_disabled_tracker
197 def test_should_not_set_a_disabled_tracker
198 p = Project.find(1)
198 p = Project.find(1)
199 p.trackers.delete(Tracker.find(2))
199 p.trackers.delete(Tracker.find(2))
200
200
201 issue = Issue.find(1)
201 issue = Issue.find(1)
202 issue.tracker_id = 2
202 issue.tracker_id = 2
203 issue.subject = 'New subject'
203 issue.subject = 'New subject'
204 assert !issue.save
204 assert !issue.save
205 assert_not_nil issue.errors.on(:tracker_id)
205 assert_not_nil issue.errors.on(:tracker_id)
206 end
206 end
207
207
208 def test_category_based_assignment
208 def test_category_based_assignment
209 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
209 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
211 end
211 end
212
212
213 def test_copy
213 def test_copy
214 issue = Issue.new.copy_from(1)
214 issue = Issue.new.copy_from(1)
215 assert issue.save
215 assert issue.save
216 issue.reload
216 issue.reload
217 orig = Issue.find(1)
217 orig = Issue.find(1)
218 assert_equal orig.subject, issue.subject
218 assert_equal orig.subject, issue.subject
219 assert_equal orig.tracker, issue.tracker
219 assert_equal orig.tracker, issue.tracker
220 assert_equal "125", issue.custom_value_for(2).value
220 assert_equal "125", issue.custom_value_for(2).value
221 end
221 end
222
222
223 def test_copy_should_copy_status
223 def test_copy_should_copy_status
224 orig = Issue.find(8)
224 orig = Issue.find(8)
225 assert orig.status != IssueStatus.default
225 assert orig.status != IssueStatus.default
226
226
227 issue = Issue.new.copy_from(orig)
227 issue = Issue.new.copy_from(orig)
228 assert issue.save
228 assert issue.save
229 issue.reload
229 issue.reload
230 assert_equal orig.status, issue.status
230 assert_equal orig.status, issue.status
231 end
231 end
232
232
233 def test_should_close_duplicates
233 def test_should_close_duplicates
234 # Create 3 issues
234 # Create 3 issues
235 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
235 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
236 assert issue1.save
236 assert issue1.save
237 issue2 = issue1.clone
237 issue2 = issue1.clone
238 assert issue2.save
238 assert issue2.save
239 issue3 = issue1.clone
239 issue3 = issue1.clone
240 assert issue3.save
240 assert issue3.save
241
241
242 # 2 is a dupe of 1
242 # 2 is a dupe of 1
243 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
243 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
244 # And 3 is a dupe of 2
244 # And 3 is a dupe of 2
245 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
245 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
246 # And 3 is a dupe of 1 (circular duplicates)
246 # And 3 is a dupe of 1 (circular duplicates)
247 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
247 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
248
248
249 assert issue1.reload.duplicates.include?(issue2)
249 assert issue1.reload.duplicates.include?(issue2)
250
250
251 # Closing issue 1
251 # Closing issue 1
252 issue1.init_journal(User.find(:first), "Closing issue1")
252 issue1.init_journal(User.find(:first), "Closing issue1")
253 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
253 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
254 assert issue1.save
254 assert issue1.save
255 # 2 and 3 should be also closed
255 # 2 and 3 should be also closed
256 assert issue2.reload.closed?
256 assert issue2.reload.closed?
257 assert issue3.reload.closed?
257 assert issue3.reload.closed?
258 end
258 end
259
259
260 def test_should_not_close_duplicated_issue
260 def test_should_not_close_duplicated_issue
261 # Create 3 issues
261 # Create 3 issues
262 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
262 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
263 assert issue1.save
263 assert issue1.save
264 issue2 = issue1.clone
264 issue2 = issue1.clone
265 assert issue2.save
265 assert issue2.save
266
266
267 # 2 is a dupe of 1
267 # 2 is a dupe of 1
268 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
268 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
269 # 2 is a dup of 1 but 1 is not a duplicate of 2
269 # 2 is a dup of 1 but 1 is not a duplicate of 2
270 assert !issue2.reload.duplicates.include?(issue1)
270 assert !issue2.reload.duplicates.include?(issue1)
271
271
272 # Closing issue 2
272 # Closing issue 2
273 issue2.init_journal(User.find(:first), "Closing issue2")
273 issue2.init_journal(User.find(:first), "Closing issue2")
274 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
274 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
275 assert issue2.save
275 assert issue2.save
276 # 1 should not be also closed
276 # 1 should not be also closed
277 assert !issue1.reload.closed?
277 assert !issue1.reload.closed?
278 end
278 end
279
279
280 def test_assignable_versions
280 def test_assignable_versions
281 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
281 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
282 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
282 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
283 end
283 end
284
284
285 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
285 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
286 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
286 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
287 assert !issue.save
287 assert !issue.save
288 assert_not_nil issue.errors.on(:fixed_version_id)
288 assert_not_nil issue.errors.on(:fixed_version_id)
289 end
289 end
290
290
291 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
291 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
292 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
292 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
293 assert !issue.save
293 assert !issue.save
294 assert_not_nil issue.errors.on(:fixed_version_id)
294 assert_not_nil issue.errors.on(:fixed_version_id)
295 end
295 end
296
296
297 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
297 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
298 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
298 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
299 assert issue.save
299 assert issue.save
300 end
300 end
301
301
302 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
302 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
303 issue = Issue.find(11)
303 issue = Issue.find(11)
304 assert_equal 'closed', issue.fixed_version.status
304 assert_equal 'closed', issue.fixed_version.status
305 issue.subject = 'Subject changed'
305 issue.subject = 'Subject changed'
306 assert issue.save
306 assert issue.save
307 end
307 end
308
308
309 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
309 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
310 issue = Issue.find(11)
310 issue = Issue.find(11)
311 issue.status_id = 1
311 issue.status_id = 1
312 assert !issue.save
312 assert !issue.save
313 assert_not_nil issue.errors.on_base
313 assert_not_nil issue.errors.on_base
314 end
314 end
315
315
316 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
316 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
317 issue = Issue.find(11)
317 issue = Issue.find(11)
318 issue.status_id = 1
318 issue.status_id = 1
319 issue.fixed_version_id = 3
319 issue.fixed_version_id = 3
320 assert issue.save
320 assert issue.save
321 end
321 end
322
322
323 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
323 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
324 issue = Issue.find(12)
324 issue = Issue.find(12)
325 assert_equal 'locked', issue.fixed_version.status
325 assert_equal 'locked', issue.fixed_version.status
326 issue.status_id = 1
326 issue.status_id = 1
327 assert issue.save
327 assert issue.save
328 end
328 end
329
329
330 def test_move_to_another_project_with_same_category
330 def test_move_to_another_project_with_same_category
331 issue = Issue.find(1)
331 issue = Issue.find(1)
332 assert issue.move_to_project(Project.find(2))
332 assert issue.move_to_project(Project.find(2))
333 issue.reload
333 issue.reload
334 assert_equal 2, issue.project_id
334 assert_equal 2, issue.project_id
335 # Category changes
335 # Category changes
336 assert_equal 4, issue.category_id
336 assert_equal 4, issue.category_id
337 # Make sure time entries were move to the target project
337 # Make sure time entries were move to the target project
338 assert_equal 2, issue.time_entries.first.project_id
338 assert_equal 2, issue.time_entries.first.project_id
339 end
339 end
340
340
341 def test_move_to_another_project_without_same_category
341 def test_move_to_another_project_without_same_category
342 issue = Issue.find(2)
342 issue = Issue.find(2)
343 assert issue.move_to_project(Project.find(2))
343 assert issue.move_to_project(Project.find(2))
344 issue.reload
344 issue.reload
345 assert_equal 2, issue.project_id
345 assert_equal 2, issue.project_id
346 # Category cleared
346 # Category cleared
347 assert_nil issue.category_id
347 assert_nil issue.category_id
348 end
348 end
349
349
350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
351 issue = Issue.find(1)
351 issue = Issue.find(1)
352 issue.update_attribute(:fixed_version_id, 1)
352 issue.update_attribute(:fixed_version_id, 1)
353 assert issue.move_to_project(Project.find(2))
353 assert issue.move_to_project(Project.find(2))
354 issue.reload
354 issue.reload
355 assert_equal 2, issue.project_id
355 assert_equal 2, issue.project_id
356 # Cleared fixed_version
356 # Cleared fixed_version
357 assert_equal nil, issue.fixed_version
357 assert_equal nil, issue.fixed_version
358 end
358 end
359
359
360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
361 issue = Issue.find(1)
361 issue = Issue.find(1)
362 issue.update_attribute(:fixed_version_id, 4)
362 issue.update_attribute(:fixed_version_id, 4)
363 assert issue.move_to_project(Project.find(5))
363 assert issue.move_to_project(Project.find(5))
364 issue.reload
364 issue.reload
365 assert_equal 5, issue.project_id
365 assert_equal 5, issue.project_id
366 # Keep fixed_version
366 # Keep fixed_version
367 assert_equal 4, issue.fixed_version_id
367 assert_equal 4, issue.fixed_version_id
368 end
368 end
369
369
370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
371 issue = Issue.find(1)
371 issue = Issue.find(1)
372 issue.update_attribute(:fixed_version_id, 1)
372 issue.update_attribute(:fixed_version_id, 1)
373 assert issue.move_to_project(Project.find(5))
373 assert issue.move_to_project(Project.find(5))
374 issue.reload
374 issue.reload
375 assert_equal 5, issue.project_id
375 assert_equal 5, issue.project_id
376 # Cleared fixed_version
376 # Cleared fixed_version
377 assert_equal nil, issue.fixed_version
377 assert_equal nil, issue.fixed_version
378 end
378 end
379
379
380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
381 issue = Issue.find(1)
381 issue = Issue.find(1)
382 issue.update_attribute(:fixed_version_id, 7)
382 issue.update_attribute(:fixed_version_id, 7)
383 assert issue.move_to_project(Project.find(2))
383 assert issue.move_to_project(Project.find(2))
384 issue.reload
384 issue.reload
385 assert_equal 2, issue.project_id
385 assert_equal 2, issue.project_id
386 # Keep fixed_version
386 # Keep fixed_version
387 assert_equal 7, issue.fixed_version_id
387 assert_equal 7, issue.fixed_version_id
388 end
388 end
389
389
390 def test_move_to_another_project_with_disabled_tracker
390 def test_move_to_another_project_with_disabled_tracker
391 issue = Issue.find(1)
391 issue = Issue.find(1)
392 target = Project.find(2)
392 target = Project.find(2)
393 target.tracker_ids = [3]
393 target.tracker_ids = [3]
394 target.save
394 target.save
395 assert_equal false, issue.move_to_project(target)
395 assert_equal false, issue.move_to_project(target)
396 issue.reload
396 issue.reload
397 assert_equal 1, issue.project_id
397 assert_equal 1, issue.project_id
398 end
398 end
399
399
400 def test_copy_to_the_same_project
400 def test_copy_to_the_same_project
401 issue = Issue.find(1)
401 issue = Issue.find(1)
402 copy = nil
402 copy = nil
403 assert_difference 'Issue.count' do
403 assert_difference 'Issue.count' do
404 copy = issue.move_to_project(issue.project, nil, :copy => true)
404 copy = issue.move_to_project(issue.project, nil, :copy => true)
405 end
405 end
406 assert_kind_of Issue, copy
406 assert_kind_of Issue, copy
407 assert_equal issue.project, copy.project
407 assert_equal issue.project, copy.project
408 assert_equal "125", copy.custom_value_for(2).value
408 assert_equal "125", copy.custom_value_for(2).value
409 end
409 end
410
410
411 def test_copy_to_another_project_and_tracker
411 def test_copy_to_another_project_and_tracker
412 issue = Issue.find(1)
412 issue = Issue.find(1)
413 copy = nil
413 copy = nil
414 assert_difference 'Issue.count' do
414 assert_difference 'Issue.count' do
415 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
415 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
416 end
416 end
417 copy.reload
417 copy.reload
418 assert_kind_of Issue, copy
418 assert_kind_of Issue, copy
419 assert_equal Project.find(3), copy.project
419 assert_equal Project.find(3), copy.project
420 assert_equal Tracker.find(2), copy.tracker
420 assert_equal Tracker.find(2), copy.tracker
421 # Custom field #2 is not associated with target tracker
421 # Custom field #2 is not associated with target tracker
422 assert_nil copy.custom_value_for(2)
422 assert_nil copy.custom_value_for(2)
423 end
423 end
424
424
425 context "#move_to_project" do
425 context "#move_to_project" do
426 context "as a copy" do
426 context "as a copy" do
427 setup do
427 setup do
428 @issue = Issue.find(1)
428 @issue = Issue.find(1)
429 @copy = nil
429 @copy = nil
430 end
430 end
431
431
432 should "allow assigned_to changes" do
432 should "allow assigned_to changes" do
433 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
433 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
434 assert_equal 3, @copy.assigned_to_id
434 assert_equal 3, @copy.assigned_to_id
435 end
435 end
436
436
437 should "allow status changes" do
437 should "allow status changes" do
438 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
438 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
439 assert_equal 2, @copy.status_id
439 assert_equal 2, @copy.status_id
440 end
440 end
441
441
442 should "allow start date changes" do
442 should "allow start date changes" do
443 date = Date.today
443 date = Date.today
444 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
444 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
445 assert_equal date, @copy.start_date
445 assert_equal date, @copy.start_date
446 end
446 end
447
447
448 should "allow due date changes" do
448 should "allow due date changes" do
449 date = Date.today
449 date = Date.today
450 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
450 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
451
451
452 assert_equal date, @copy.due_date
452 assert_equal date, @copy.due_date
453 end
453 end
454 end
454 end
455 end
455 end
456
456
457 def test_recipients_should_not_include_users_that_cannot_view_the_issue
457 def test_recipients_should_not_include_users_that_cannot_view_the_issue
458 issue = Issue.find(12)
458 issue = Issue.find(12)
459 assert issue.recipients.include?(issue.author.mail)
459 assert issue.recipients.include?(issue.author.mail)
460 # move the issue to a private project
460 # move the issue to a private project
461 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
461 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
462 # author is not a member of project anymore
462 # author is not a member of project anymore
463 assert !copy.recipients.include?(copy.author.mail)
463 assert !copy.recipients.include?(copy.author.mail)
464 end
464 end
465
465
466 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
466 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
467 user = User.find(3)
467 user = User.find(3)
468 issue = Issue.find(9)
468 issue = Issue.find(9)
469 Watcher.create!(:user => user, :watchable => issue)
469 Watcher.create!(:user => user, :watchable => issue)
470 assert issue.watched_by?(user)
470 assert issue.watched_by?(user)
471 assert !issue.watcher_recipients.include?(user.mail)
471 assert !issue.watcher_recipients.include?(user.mail)
472 end
472 end
473
473
474 def test_issue_destroy
474 def test_issue_destroy
475 Issue.find(1).destroy
475 Issue.find(1).destroy
476 assert_nil Issue.find_by_id(1)
476 assert_nil Issue.find_by_id(1)
477 assert_nil TimeEntry.find_by_issue_id(1)
477 assert_nil TimeEntry.find_by_issue_id(1)
478 end
478 end
479
479
480 def test_blocked
480 def test_blocked
481 blocked_issue = Issue.find(9)
481 blocked_issue = Issue.find(9)
482 blocking_issue = Issue.find(10)
482 blocking_issue = Issue.find(10)
483
483
484 assert blocked_issue.blocked?
484 assert blocked_issue.blocked?
485 assert !blocking_issue.blocked?
485 assert !blocking_issue.blocked?
486 end
486 end
487
487
488 def test_blocked_issues_dont_allow_closed_statuses
488 def test_blocked_issues_dont_allow_closed_statuses
489 blocked_issue = Issue.find(9)
489 blocked_issue = Issue.find(9)
490
490
491 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
491 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
492 assert !allowed_statuses.empty?
492 assert !allowed_statuses.empty?
493 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
493 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
494 assert closed_statuses.empty?
494 assert closed_statuses.empty?
495 end
495 end
496
496
497 def test_unblocked_issues_allow_closed_statuses
497 def test_unblocked_issues_allow_closed_statuses
498 blocking_issue = Issue.find(10)
498 blocking_issue = Issue.find(10)
499
499
500 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
500 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
501 assert !allowed_statuses.empty?
501 assert !allowed_statuses.empty?
502 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
502 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
503 assert !closed_statuses.empty?
503 assert !closed_statuses.empty?
504 end
504 end
505
505
506 def test_rescheduling_an_issue_should_reschedule_following_issue
507 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
508 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
509 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
510 assert_equal issue1.due_date + 1, issue2.reload.start_date
511
512 issue1.due_date = Date.today + 5
513 issue1.save!
514 assert_equal issue1.due_date + 1, issue2.reload.start_date
515 end
516
506 def test_overdue
517 def test_overdue
507 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
518 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
508 assert !Issue.new(:due_date => Date.today).overdue?
519 assert !Issue.new(:due_date => Date.today).overdue?
509 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
520 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
510 assert !Issue.new(:due_date => nil).overdue?
521 assert !Issue.new(:due_date => nil).overdue?
511 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
522 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
512 end
523 end
513
524
514 context "#assignable_users" do
525 context "#assignable_users" do
515 should "be Users" do
526 should "be Users" do
516 assert_kind_of User, Issue.find(1).assignable_users.first
527 assert_kind_of User, Issue.find(1).assignable_users.first
517 end
528 end
518
529
519 should "include the issue author" do
530 should "include the issue author" do
520 project = Project.find(1)
531 project = Project.find(1)
521 non_project_member = User.generate!
532 non_project_member = User.generate!
522 issue = Issue.generate_for_project!(project, :author => non_project_member)
533 issue = Issue.generate_for_project!(project, :author => non_project_member)
523
534
524 assert issue.assignable_users.include?(non_project_member)
535 assert issue.assignable_users.include?(non_project_member)
525 end
536 end
526
537
527 should "not show the issue author twice" do
538 should "not show the issue author twice" do
528 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
539 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
529 assert_equal 2, assignable_user_ids.length
540 assert_equal 2, assignable_user_ids.length
530
541
531 assignable_user_ids.each do |user_id|
542 assignable_user_ids.each do |user_id|
532 assert_equal 1, assignable_user_ids.count(user_id), "User #{user_id} appears more or less than once"
543 assert_equal 1, assignable_user_ids.count(user_id), "User #{user_id} appears more or less than once"
533 end
544 end
534 end
545 end
535 end
546 end
536
547
537 def test_create_should_send_email_notification
548 def test_create_should_send_email_notification
538 ActionMailer::Base.deliveries.clear
549 ActionMailer::Base.deliveries.clear
539 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
550 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
540
551
541 assert issue.save
552 assert issue.save
542 assert_equal 1, ActionMailer::Base.deliveries.size
553 assert_equal 1, ActionMailer::Base.deliveries.size
543 end
554 end
544
555
545 def test_stale_issue_should_not_send_email_notification
556 def test_stale_issue_should_not_send_email_notification
546 ActionMailer::Base.deliveries.clear
557 ActionMailer::Base.deliveries.clear
547 issue = Issue.find(1)
558 issue = Issue.find(1)
548 stale = Issue.find(1)
559 stale = Issue.find(1)
549
560
550 issue.init_journal(User.find(1))
561 issue.init_journal(User.find(1))
551 issue.subject = 'Subjet update'
562 issue.subject = 'Subjet update'
552 assert issue.save
563 assert issue.save
553 assert_equal 1, ActionMailer::Base.deliveries.size
564 assert_equal 1, ActionMailer::Base.deliveries.size
554 ActionMailer::Base.deliveries.clear
565 ActionMailer::Base.deliveries.clear
555
566
556 stale.init_journal(User.find(1))
567 stale.init_journal(User.find(1))
557 stale.subject = 'Another subjet update'
568 stale.subject = 'Another subjet update'
558 assert_raise ActiveRecord::StaleObjectError do
569 assert_raise ActiveRecord::StaleObjectError do
559 stale.save
570 stale.save
560 end
571 end
561 assert ActionMailer::Base.deliveries.empty?
572 assert ActionMailer::Base.deliveries.empty?
562 end
573 end
563
574
564 def test_saving_twice_should_not_duplicate_journal_details
575 def test_saving_twice_should_not_duplicate_journal_details
565 i = Issue.find(:first)
576 i = Issue.find(:first)
566 i.init_journal(User.find(2), 'Some notes')
577 i.init_journal(User.find(2), 'Some notes')
567 # initial changes
578 # initial changes
568 i.subject = 'New subject'
579 i.subject = 'New subject'
569 i.done_ratio = i.done_ratio + 10
580 i.done_ratio = i.done_ratio + 10
570 assert_difference 'Journal.count' do
581 assert_difference 'Journal.count' do
571 assert i.save
582 assert i.save
572 end
583 end
573 # 1 more change
584 # 1 more change
574 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
585 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
575 assert_no_difference 'Journal.count' do
586 assert_no_difference 'Journal.count' do
576 assert_difference 'JournalDetail.count', 1 do
587 assert_difference 'JournalDetail.count', 1 do
577 i.save
588 i.save
578 end
589 end
579 end
590 end
580 # no more change
591 # no more change
581 assert_no_difference 'Journal.count' do
592 assert_no_difference 'Journal.count' do
582 assert_no_difference 'JournalDetail.count' do
593 assert_no_difference 'JournalDetail.count' do
583 i.save
594 i.save
584 end
595 end
585 end
596 end
586 end
597 end
587
598
588 context "#done_ratio" do
599 context "#done_ratio" do
589 setup do
600 setup do
590 @issue = Issue.find(1)
601 @issue = Issue.find(1)
591 @issue_status = IssueStatus.find(1)
602 @issue_status = IssueStatus.find(1)
592 @issue_status.update_attribute(:default_done_ratio, 50)
603 @issue_status.update_attribute(:default_done_ratio, 50)
593 @issue2 = Issue.find(2)
604 @issue2 = Issue.find(2)
594 @issue_status2 = IssueStatus.find(2)
605 @issue_status2 = IssueStatus.find(2)
595 @issue_status2.update_attribute(:default_done_ratio, 0)
606 @issue_status2.update_attribute(:default_done_ratio, 0)
596 end
607 end
597
608
598 context "with Setting.issue_done_ratio using the issue_field" do
609 context "with Setting.issue_done_ratio using the issue_field" do
599 setup do
610 setup do
600 Setting.issue_done_ratio = 'issue_field'
611 Setting.issue_done_ratio = 'issue_field'
601 end
612 end
602
613
603 should "read the issue's field" do
614 should "read the issue's field" do
604 assert_equal 0, @issue.done_ratio
615 assert_equal 0, @issue.done_ratio
605 assert_equal 30, @issue2.done_ratio
616 assert_equal 30, @issue2.done_ratio
606 end
617 end
607 end
618 end
608
619
609 context "with Setting.issue_done_ratio using the issue_status" do
620 context "with Setting.issue_done_ratio using the issue_status" do
610 setup do
621 setup do
611 Setting.issue_done_ratio = 'issue_status'
622 Setting.issue_done_ratio = 'issue_status'
612 end
623 end
613
624
614 should "read the Issue Status's default done ratio" do
625 should "read the Issue Status's default done ratio" do
615 assert_equal 50, @issue.done_ratio
626 assert_equal 50, @issue.done_ratio
616 assert_equal 0, @issue2.done_ratio
627 assert_equal 0, @issue2.done_ratio
617 end
628 end
618 end
629 end
619 end
630 end
620
631
621 context "#update_done_ratio_from_issue_status" do
632 context "#update_done_ratio_from_issue_status" do
622 setup do
633 setup do
623 @issue = Issue.find(1)
634 @issue = Issue.find(1)
624 @issue_status = IssueStatus.find(1)
635 @issue_status = IssueStatus.find(1)
625 @issue_status.update_attribute(:default_done_ratio, 50)
636 @issue_status.update_attribute(:default_done_ratio, 50)
626 @issue2 = Issue.find(2)
637 @issue2 = Issue.find(2)
627 @issue_status2 = IssueStatus.find(2)
638 @issue_status2 = IssueStatus.find(2)
628 @issue_status2.update_attribute(:default_done_ratio, 0)
639 @issue_status2.update_attribute(:default_done_ratio, 0)
629 end
640 end
630
641
631 context "with Setting.issue_done_ratio using the issue_field" do
642 context "with Setting.issue_done_ratio using the issue_field" do
632 setup do
643 setup do
633 Setting.issue_done_ratio = 'issue_field'
644 Setting.issue_done_ratio = 'issue_field'
634 end
645 end
635
646
636 should "not change the issue" do
647 should "not change the issue" do
637 @issue.update_done_ratio_from_issue_status
648 @issue.update_done_ratio_from_issue_status
638 @issue2.update_done_ratio_from_issue_status
649 @issue2.update_done_ratio_from_issue_status
639
650
640 assert_equal 0, @issue.read_attribute(:done_ratio)
651 assert_equal 0, @issue.read_attribute(:done_ratio)
641 assert_equal 30, @issue2.read_attribute(:done_ratio)
652 assert_equal 30, @issue2.read_attribute(:done_ratio)
642 end
653 end
643 end
654 end
644
655
645 context "with Setting.issue_done_ratio using the issue_status" do
656 context "with Setting.issue_done_ratio using the issue_status" do
646 setup do
657 setup do
647 Setting.issue_done_ratio = 'issue_status'
658 Setting.issue_done_ratio = 'issue_status'
648 end
659 end
649
660
650 should "change the issue's done ratio" do
661 should "change the issue's done ratio" do
651 @issue.update_done_ratio_from_issue_status
662 @issue.update_done_ratio_from_issue_status
652 @issue2.update_done_ratio_from_issue_status
663 @issue2.update_done_ratio_from_issue_status
653
664
654 assert_equal 50, @issue.read_attribute(:done_ratio)
665 assert_equal 50, @issue.read_attribute(:done_ratio)
655 assert_equal 0, @issue2.read_attribute(:done_ratio)
666 assert_equal 0, @issue2.read_attribute(:done_ratio)
656 end
667 end
657 end
668 end
658 end
669 end
659
670
660 test "#by_tracker" do
671 test "#by_tracker" do
661 groups = Issue.by_tracker(Project.find(1))
672 groups = Issue.by_tracker(Project.find(1))
662 assert_equal 3, groups.size
673 assert_equal 3, groups.size
663 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
674 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
664 end
675 end
665
676
666 test "#by_version" do
677 test "#by_version" do
667 groups = Issue.by_version(Project.find(1))
678 groups = Issue.by_version(Project.find(1))
668 assert_equal 3, groups.size
679 assert_equal 3, groups.size
669 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
680 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
670 end
681 end
671
682
672 test "#by_priority" do
683 test "#by_priority" do
673 groups = Issue.by_priority(Project.find(1))
684 groups = Issue.by_priority(Project.find(1))
674 assert_equal 4, groups.size
685 assert_equal 4, groups.size
675 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
686 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
676 end
687 end
677
688
678 test "#by_category" do
689 test "#by_category" do
679 groups = Issue.by_category(Project.find(1))
690 groups = Issue.by_category(Project.find(1))
680 assert_equal 2, groups.size
691 assert_equal 2, groups.size
681 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
692 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
682 end
693 end
683
694
684 test "#by_assigned_to" do
695 test "#by_assigned_to" do
685 groups = Issue.by_assigned_to(Project.find(1))
696 groups = Issue.by_assigned_to(Project.find(1))
686 assert_equal 2, groups.size
697 assert_equal 2, groups.size
687 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
698 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
688 end
699 end
689
700
690 test "#by_author" do
701 test "#by_author" do
691 groups = Issue.by_author(Project.find(1))
702 groups = Issue.by_author(Project.find(1))
692 assert_equal 4, groups.size
703 assert_equal 4, groups.size
693 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
704 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
694 end
705 end
695
706
696 test "#by_subproject" do
707 test "#by_subproject" do
697 groups = Issue.by_subproject(Project.find(1))
708 groups = Issue.by_subproject(Project.find(1))
698 assert_equal 2, groups.size
709 assert_equal 2, groups.size
699 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
710 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
700 end
711 end
701
712
702
713
703 context ".allowed_target_projects_on_move" do
714 context ".allowed_target_projects_on_move" do
704 should "return all active projects for admin users" do
715 should "return all active projects for admin users" do
705 User.current = User.find(1)
716 User.current = User.find(1)
706 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
717 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
707 end
718 end
708
719
709 should "return allowed projects for non admin users" do
720 should "return allowed projects for non admin users" do
710 User.current = User.find(2)
721 User.current = User.find(2)
711 Role.non_member.remove_permission! :move_issues
722 Role.non_member.remove_permission! :move_issues
712 assert_equal 3, Issue.allowed_target_projects_on_move.size
723 assert_equal 3, Issue.allowed_target_projects_on_move.size
713
724
714 Role.non_member.add_permission! :move_issues
725 Role.non_member.add_permission! :move_issues
715 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
726 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
716 end
727 end
717 end
728 end
718
729
719 def test_recently_updated_with_limit_scopes
730 def test_recently_updated_with_limit_scopes
720 #should return the last updated issue
731 #should return the last updated issue
721 assert_equal 1, Issue.recently_updated.with_limit(1).length
732 assert_equal 1, Issue.recently_updated.with_limit(1).length
722 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
733 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
723 end
734 end
724
735
725 def test_on_active_projects_scope
736 def test_on_active_projects_scope
726 assert Project.find(2).archive
737 assert Project.find(2).archive
727
738
728 before = Issue.on_active_project.length
739 before = Issue.on_active_project.length
729 # test inclusion to results
740 # test inclusion to results
730 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
741 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
731 assert_equal before + 1, Issue.on_active_project.length
742 assert_equal before + 1, Issue.on_active_project.length
732
743
733 # Move to an archived project
744 # Move to an archived project
734 issue.project = Project.find(2)
745 issue.project = Project.find(2)
735 assert issue.save
746 assert issue.save
736 assert_equal before, Issue.on_active_project.length
747 assert_equal before, Issue.on_active_project.length
737 end
748 end
738 end
749 end
General Comments 0
You need to be logged in to leave comments. Login now