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