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