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