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