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