##// END OF EJS Templates
Makes all workflow transitions defined for all roles available to administrators (#2323)....
Jean-Philippe Lang -
r8587:ffbd3c0522a0
parent child
Show More
@@ -1,1052 +1,1052
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_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
432 self.custom_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 # Return true if the issue is closed, otherwise false
439 # Return true if the issue is closed, otherwise false
440 def closed?
440 def closed?
441 self.status.is_closed?
441 self.status.is_closed?
442 end
442 end
443
443
444 # Return true if the issue is being reopened
444 # Return true if the issue is being reopened
445 def reopened?
445 def reopened?
446 if !new_record? && status_id_changed?
446 if !new_record? && status_id_changed?
447 status_was = IssueStatus.find_by_id(status_id_was)
447 status_was = IssueStatus.find_by_id(status_id_was)
448 status_new = IssueStatus.find_by_id(status_id)
448 status_new = IssueStatus.find_by_id(status_id)
449 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
449 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
450 return true
450 return true
451 end
451 end
452 end
452 end
453 false
453 false
454 end
454 end
455
455
456 # Return true if the issue is being closed
456 # Return true if the issue is being closed
457 def closing?
457 def closing?
458 if !new_record? && status_id_changed?
458 if !new_record? && status_id_changed?
459 status_was = IssueStatus.find_by_id(status_id_was)
459 status_was = IssueStatus.find_by_id(status_id_was)
460 status_new = IssueStatus.find_by_id(status_id)
460 status_new = IssueStatus.find_by_id(status_id)
461 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
461 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
462 return true
462 return true
463 end
463 end
464 end
464 end
465 false
465 false
466 end
466 end
467
467
468 # Returns true if the issue is overdue
468 # Returns true if the issue is overdue
469 def overdue?
469 def overdue?
470 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
470 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
471 end
471 end
472
472
473 # Is the amount of work done less than it should for the due date
473 # Is the amount of work done less than it should for the due date
474 def behind_schedule?
474 def behind_schedule?
475 return false if start_date.nil? || due_date.nil?
475 return false if start_date.nil? || due_date.nil?
476 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
476 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
477 return done_date <= Date.today
477 return done_date <= Date.today
478 end
478 end
479
479
480 # Does this issue have children?
480 # Does this issue have children?
481 def children?
481 def children?
482 !leaf?
482 !leaf?
483 end
483 end
484
484
485 # Users the issue can be assigned to
485 # Users the issue can be assigned to
486 def assignable_users
486 def assignable_users
487 users = project.assignable_users
487 users = project.assignable_users
488 users << author if author
488 users << author if author
489 users << assigned_to if assigned_to
489 users << assigned_to if assigned_to
490 users.uniq.sort
490 users.uniq.sort
491 end
491 end
492
492
493 # Versions that the issue can be assigned to
493 # Versions that the issue can be assigned to
494 def assignable_versions
494 def assignable_versions
495 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
495 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
496 end
496 end
497
497
498 # Returns true if this issue is blocked by another issue that is still open
498 # Returns true if this issue is blocked by another issue that is still open
499 def blocked?
499 def blocked?
500 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
500 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
501 end
501 end
502
502
503 # Returns an array of status that user is able to apply
503 # Returns an array of status that user is able to apply
504 def new_statuses_allowed_to(user, include_default=false)
504 def new_statuses_allowed_to(user, include_default=false)
505 statuses = status.find_new_statuses_allowed_to(
505 statuses = status.find_new_statuses_allowed_to(
506 user.roles_for_project(project),
506 user.admin ? Role.all : user.roles_for_project(project),
507 tracker,
507 tracker,
508 author == user,
508 author == user,
509 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
509 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
510 )
510 )
511 statuses << status unless statuses.empty?
511 statuses << status unless statuses.empty?
512 statuses << IssueStatus.default if include_default
512 statuses << IssueStatus.default if include_default
513 statuses = statuses.uniq.sort
513 statuses = statuses.uniq.sort
514 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
514 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
515 end
515 end
516
516
517 def assigned_to_was
517 def assigned_to_was
518 if assigned_to_id_changed? && assigned_to_id_was.present?
518 if assigned_to_id_changed? && assigned_to_id_was.present?
519 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
519 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
520 end
520 end
521 end
521 end
522
522
523 # Returns the mail adresses of users that should be notified
523 # Returns the mail adresses of users that should be notified
524 def recipients
524 def recipients
525 notified = []
525 notified = []
526 # Author and assignee are always notified unless they have been
526 # Author and assignee are always notified unless they have been
527 # locked or don't want to be notified
527 # locked or don't want to be notified
528 notified << author if author
528 notified << author if author
529 if assigned_to
529 if assigned_to
530 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
530 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
531 end
531 end
532 if assigned_to_was
532 if assigned_to_was
533 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
533 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
534 end
534 end
535 notified = notified.select {|u| u.active? && u.notify_about?(self)}
535 notified = notified.select {|u| u.active? && u.notify_about?(self)}
536
536
537 notified += project.notified_users
537 notified += project.notified_users
538 notified.uniq!
538 notified.uniq!
539 # Remove users that can not view the issue
539 # Remove users that can not view the issue
540 notified.reject! {|user| !visible?(user)}
540 notified.reject! {|user| !visible?(user)}
541 notified.collect(&:mail)
541 notified.collect(&:mail)
542 end
542 end
543
543
544 # Returns the number of hours spent on this issue
544 # Returns the number of hours spent on this issue
545 def spent_hours
545 def spent_hours
546 @spent_hours ||= time_entries.sum(:hours) || 0
546 @spent_hours ||= time_entries.sum(:hours) || 0
547 end
547 end
548
548
549 # Returns the total number of hours spent on this issue and its descendants
549 # Returns the total number of hours spent on this issue and its descendants
550 #
550 #
551 # Example:
551 # Example:
552 # spent_hours => 0.0
552 # spent_hours => 0.0
553 # spent_hours => 50.2
553 # spent_hours => 50.2
554 def total_spent_hours
554 def total_spent_hours
555 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
555 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
556 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
556 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
557 end
557 end
558
558
559 def relations
559 def relations
560 @relations ||= (relations_from + relations_to).sort
560 @relations ||= (relations_from + relations_to).sort
561 end
561 end
562
562
563 # Preloads relations for a collection of issues
563 # Preloads relations for a collection of issues
564 def self.load_relations(issues)
564 def self.load_relations(issues)
565 if issues.any?
565 if issues.any?
566 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
566 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
567 issues.each do |issue|
567 issues.each do |issue|
568 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
568 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
569 end
569 end
570 end
570 end
571 end
571 end
572
572
573 # Preloads visible spent time for a collection of issues
573 # Preloads visible spent time for a collection of issues
574 def self.load_visible_spent_hours(issues, user=User.current)
574 def self.load_visible_spent_hours(issues, user=User.current)
575 if issues.any?
575 if issues.any?
576 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
576 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
577 issues.each do |issue|
577 issues.each do |issue|
578 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
578 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
579 end
579 end
580 end
580 end
581 end
581 end
582
582
583 # Finds an issue relation given its id.
583 # Finds an issue relation given its id.
584 def find_relation(relation_id)
584 def find_relation(relation_id)
585 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
585 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
586 end
586 end
587
587
588 def all_dependent_issues(except=[])
588 def all_dependent_issues(except=[])
589 except << self
589 except << self
590 dependencies = []
590 dependencies = []
591 relations_from.each do |relation|
591 relations_from.each do |relation|
592 if relation.issue_to && !except.include?(relation.issue_to)
592 if relation.issue_to && !except.include?(relation.issue_to)
593 dependencies << relation.issue_to
593 dependencies << relation.issue_to
594 dependencies += relation.issue_to.all_dependent_issues(except)
594 dependencies += relation.issue_to.all_dependent_issues(except)
595 end
595 end
596 end
596 end
597 dependencies
597 dependencies
598 end
598 end
599
599
600 # Returns an array of issues that duplicate this one
600 # Returns an array of issues that duplicate this one
601 def duplicates
601 def duplicates
602 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
602 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
603 end
603 end
604
604
605 # Returns the due date or the target due date if any
605 # Returns the due date or the target due date if any
606 # Used on gantt chart
606 # Used on gantt chart
607 def due_before
607 def due_before
608 due_date || (fixed_version ? fixed_version.effective_date : nil)
608 due_date || (fixed_version ? fixed_version.effective_date : nil)
609 end
609 end
610
610
611 # Returns the time scheduled for this issue.
611 # Returns the time scheduled for this issue.
612 #
612 #
613 # Example:
613 # Example:
614 # Start Date: 2/26/09, End Date: 3/04/09
614 # Start Date: 2/26/09, End Date: 3/04/09
615 # duration => 6
615 # duration => 6
616 def duration
616 def duration
617 (start_date && due_date) ? due_date - start_date : 0
617 (start_date && due_date) ? due_date - start_date : 0
618 end
618 end
619
619
620 def soonest_start
620 def soonest_start
621 @soonest_start ||= (
621 @soonest_start ||= (
622 relations_to.collect{|relation| relation.successor_soonest_start} +
622 relations_to.collect{|relation| relation.successor_soonest_start} +
623 ancestors.collect(&:soonest_start)
623 ancestors.collect(&:soonest_start)
624 ).compact.max
624 ).compact.max
625 end
625 end
626
626
627 def reschedule_after(date)
627 def reschedule_after(date)
628 return if date.nil?
628 return if date.nil?
629 if leaf?
629 if leaf?
630 if start_date.nil? || start_date < date
630 if start_date.nil? || start_date < date
631 self.start_date, self.due_date = date, date + duration
631 self.start_date, self.due_date = date, date + duration
632 save
632 save
633 end
633 end
634 else
634 else
635 leaves.each do |leaf|
635 leaves.each do |leaf|
636 leaf.reschedule_after(date)
636 leaf.reschedule_after(date)
637 end
637 end
638 end
638 end
639 end
639 end
640
640
641 def <=>(issue)
641 def <=>(issue)
642 if issue.nil?
642 if issue.nil?
643 -1
643 -1
644 elsif root_id != issue.root_id
644 elsif root_id != issue.root_id
645 (root_id || 0) <=> (issue.root_id || 0)
645 (root_id || 0) <=> (issue.root_id || 0)
646 else
646 else
647 (lft || 0) <=> (issue.lft || 0)
647 (lft || 0) <=> (issue.lft || 0)
648 end
648 end
649 end
649 end
650
650
651 def to_s
651 def to_s
652 "#{tracker} ##{id}: #{subject}"
652 "#{tracker} ##{id}: #{subject}"
653 end
653 end
654
654
655 # Returns a string of css classes that apply to the issue
655 # Returns a string of css classes that apply to the issue
656 def css_classes
656 def css_classes
657 s = "issue status-#{status.position} priority-#{priority.position}"
657 s = "issue status-#{status.position} priority-#{priority.position}"
658 s << ' closed' if closed?
658 s << ' closed' if closed?
659 s << ' overdue' if overdue?
659 s << ' overdue' if overdue?
660 s << ' child' if child?
660 s << ' child' if child?
661 s << ' parent' unless leaf?
661 s << ' parent' unless leaf?
662 s << ' private' if is_private?
662 s << ' private' if is_private?
663 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
663 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
664 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
664 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
665 s
665 s
666 end
666 end
667
667
668 # Saves an issue, time_entry, attachments, and a journal from the parameters
668 # Saves an issue, time_entry, attachments, and a journal from the parameters
669 # Returns false if save fails
669 # Returns false if save fails
670 def save_issue_with_child_records(params, existing_time_entry=nil)
670 def save_issue_with_child_records(params, existing_time_entry=nil)
671 Issue.transaction do
671 Issue.transaction do
672 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
672 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
673 @time_entry = existing_time_entry || TimeEntry.new
673 @time_entry = existing_time_entry || TimeEntry.new
674 @time_entry.project = project
674 @time_entry.project = project
675 @time_entry.issue = self
675 @time_entry.issue = self
676 @time_entry.user = User.current
676 @time_entry.user = User.current
677 @time_entry.spent_on = User.current.today
677 @time_entry.spent_on = User.current.today
678 @time_entry.attributes = params[:time_entry]
678 @time_entry.attributes = params[:time_entry]
679 self.time_entries << @time_entry
679 self.time_entries << @time_entry
680 end
680 end
681
681
682 if valid?
682 if valid?
683 attachments = Attachment.attach_files(self, params[:attachments])
683 attachments = Attachment.attach_files(self, params[:attachments])
684 # TODO: Rename hook
684 # TODO: Rename hook
685 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
685 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
686 begin
686 begin
687 if save
687 if save
688 # TODO: Rename hook
688 # TODO: Rename hook
689 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
689 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
690 else
690 else
691 raise ActiveRecord::Rollback
691 raise ActiveRecord::Rollback
692 end
692 end
693 rescue ActiveRecord::StaleObjectError
693 rescue ActiveRecord::StaleObjectError
694 attachments[:files].each(&:destroy)
694 attachments[:files].each(&:destroy)
695 errors.add :base, l(:notice_locking_conflict)
695 errors.add :base, l(:notice_locking_conflict)
696 raise ActiveRecord::Rollback
696 raise ActiveRecord::Rollback
697 end
697 end
698 end
698 end
699 end
699 end
700 end
700 end
701
701
702 # Unassigns issues from +version+ if it's no longer shared with issue's project
702 # Unassigns issues from +version+ if it's no longer shared with issue's project
703 def self.update_versions_from_sharing_change(version)
703 def self.update_versions_from_sharing_change(version)
704 # Update issues assigned to the version
704 # Update issues assigned to the version
705 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
705 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
706 end
706 end
707
707
708 # Unassigns issues from versions that are no longer shared
708 # Unassigns issues from versions that are no longer shared
709 # after +project+ was moved
709 # after +project+ was moved
710 def self.update_versions_from_hierarchy_change(project)
710 def self.update_versions_from_hierarchy_change(project)
711 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
711 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
712 # Update issues of the moved projects and issues assigned to a version of a moved project
712 # Update issues of the moved projects and issues assigned to a version of a moved project
713 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
713 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
714 end
714 end
715
715
716 def parent_issue_id=(arg)
716 def parent_issue_id=(arg)
717 parent_issue_id = arg.blank? ? nil : arg.to_i
717 parent_issue_id = arg.blank? ? nil : arg.to_i
718 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
718 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
719 @parent_issue.id
719 @parent_issue.id
720 else
720 else
721 @parent_issue = nil
721 @parent_issue = nil
722 nil
722 nil
723 end
723 end
724 end
724 end
725
725
726 def parent_issue_id
726 def parent_issue_id
727 if instance_variable_defined? :@parent_issue
727 if instance_variable_defined? :@parent_issue
728 @parent_issue.nil? ? nil : @parent_issue.id
728 @parent_issue.nil? ? nil : @parent_issue.id
729 else
729 else
730 parent_id
730 parent_id
731 end
731 end
732 end
732 end
733
733
734 # Extracted from the ReportsController.
734 # Extracted from the ReportsController.
735 def self.by_tracker(project)
735 def self.by_tracker(project)
736 count_and_group_by(:project => project,
736 count_and_group_by(:project => project,
737 :field => 'tracker_id',
737 :field => 'tracker_id',
738 :joins => Tracker.table_name)
738 :joins => Tracker.table_name)
739 end
739 end
740
740
741 def self.by_version(project)
741 def self.by_version(project)
742 count_and_group_by(:project => project,
742 count_and_group_by(:project => project,
743 :field => 'fixed_version_id',
743 :field => 'fixed_version_id',
744 :joins => Version.table_name)
744 :joins => Version.table_name)
745 end
745 end
746
746
747 def self.by_priority(project)
747 def self.by_priority(project)
748 count_and_group_by(:project => project,
748 count_and_group_by(:project => project,
749 :field => 'priority_id',
749 :field => 'priority_id',
750 :joins => IssuePriority.table_name)
750 :joins => IssuePriority.table_name)
751 end
751 end
752
752
753 def self.by_category(project)
753 def self.by_category(project)
754 count_and_group_by(:project => project,
754 count_and_group_by(:project => project,
755 :field => 'category_id',
755 :field => 'category_id',
756 :joins => IssueCategory.table_name)
756 :joins => IssueCategory.table_name)
757 end
757 end
758
758
759 def self.by_assigned_to(project)
759 def self.by_assigned_to(project)
760 count_and_group_by(:project => project,
760 count_and_group_by(:project => project,
761 :field => 'assigned_to_id',
761 :field => 'assigned_to_id',
762 :joins => User.table_name)
762 :joins => User.table_name)
763 end
763 end
764
764
765 def self.by_author(project)
765 def self.by_author(project)
766 count_and_group_by(:project => project,
766 count_and_group_by(:project => project,
767 :field => 'author_id',
767 :field => 'author_id',
768 :joins => User.table_name)
768 :joins => User.table_name)
769 end
769 end
770
770
771 def self.by_subproject(project)
771 def self.by_subproject(project)
772 ActiveRecord::Base.connection.select_all("select s.id as status_id,
772 ActiveRecord::Base.connection.select_all("select s.id as status_id,
773 s.is_closed as closed,
773 s.is_closed as closed,
774 #{Issue.table_name}.project_id as project_id,
774 #{Issue.table_name}.project_id as project_id,
775 count(#{Issue.table_name}.id) as total
775 count(#{Issue.table_name}.id) as total
776 from
776 from
777 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
777 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
778 where
778 where
779 #{Issue.table_name}.status_id=s.id
779 #{Issue.table_name}.status_id=s.id
780 and #{Issue.table_name}.project_id = #{Project.table_name}.id
780 and #{Issue.table_name}.project_id = #{Project.table_name}.id
781 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
781 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
782 and #{Issue.table_name}.project_id <> #{project.id}
782 and #{Issue.table_name}.project_id <> #{project.id}
783 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
783 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
784 end
784 end
785 # End ReportsController extraction
785 # End ReportsController extraction
786
786
787 # Returns an array of projects that user can assign the issue to
787 # Returns an array of projects that user can assign the issue to
788 def allowed_target_projects(user=User.current)
788 def allowed_target_projects(user=User.current)
789 if new_record?
789 if new_record?
790 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
790 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
791 else
791 else
792 self.class.allowed_target_projects_on_move(user)
792 self.class.allowed_target_projects_on_move(user)
793 end
793 end
794 end
794 end
795
795
796 # Returns an array of projects that user can move issues to
796 # Returns an array of projects that user can move issues to
797 def self.allowed_target_projects_on_move(user=User.current)
797 def self.allowed_target_projects_on_move(user=User.current)
798 projects = []
798 projects = []
799 if user.admin?
799 if user.admin?
800 # admin is allowed to move issues to any active (visible) project
800 # admin is allowed to move issues to any active (visible) project
801 projects = Project.visible(user).all
801 projects = Project.visible(user).all
802 elsif user.logged?
802 elsif user.logged?
803 if Role.non_member.allowed_to?(:move_issues)
803 if Role.non_member.allowed_to?(:move_issues)
804 projects = Project.visible(user).all
804 projects = Project.visible(user).all
805 else
805 else
806 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
806 user.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
807 end
807 end
808 end
808 end
809 projects
809 projects
810 end
810 end
811
811
812 private
812 private
813
813
814 def after_project_change
814 def after_project_change
815 # Update project_id on related time entries
815 # Update project_id on related time entries
816 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
816 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
817
817
818 # Delete issue relations
818 # Delete issue relations
819 unless Setting.cross_project_issue_relations?
819 unless Setting.cross_project_issue_relations?
820 relations_from.clear
820 relations_from.clear
821 relations_to.clear
821 relations_to.clear
822 end
822 end
823
823
824 # Move subtasks
824 # Move subtasks
825 children.each do |child|
825 children.each do |child|
826 # Change project and keep project
826 # Change project and keep project
827 child.send :project=, project, true
827 child.send :project=, project, true
828 unless child.save
828 unless child.save
829 raise ActiveRecord::Rollback
829 raise ActiveRecord::Rollback
830 end
830 end
831 end
831 end
832 end
832 end
833
833
834 def update_nested_set_attributes
834 def update_nested_set_attributes
835 if root_id.nil?
835 if root_id.nil?
836 # issue was just created
836 # issue was just created
837 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
837 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
838 set_default_left_and_right
838 set_default_left_and_right
839 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
839 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
840 if @parent_issue
840 if @parent_issue
841 move_to_child_of(@parent_issue)
841 move_to_child_of(@parent_issue)
842 end
842 end
843 reload
843 reload
844 elsif parent_issue_id != parent_id
844 elsif parent_issue_id != parent_id
845 former_parent_id = parent_id
845 former_parent_id = parent_id
846 # moving an existing issue
846 # moving an existing issue
847 if @parent_issue && @parent_issue.root_id == root_id
847 if @parent_issue && @parent_issue.root_id == root_id
848 # inside the same tree
848 # inside the same tree
849 move_to_child_of(@parent_issue)
849 move_to_child_of(@parent_issue)
850 else
850 else
851 # to another tree
851 # to another tree
852 unless root?
852 unless root?
853 move_to_right_of(root)
853 move_to_right_of(root)
854 reload
854 reload
855 end
855 end
856 old_root_id = root_id
856 old_root_id = root_id
857 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
857 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
858 target_maxright = nested_set_scope.maximum(right_column_name) || 0
858 target_maxright = nested_set_scope.maximum(right_column_name) || 0
859 offset = target_maxright + 1 - lft
859 offset = target_maxright + 1 - lft
860 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
860 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
861 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
861 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
862 self[left_column_name] = lft + offset
862 self[left_column_name] = lft + offset
863 self[right_column_name] = rgt + offset
863 self[right_column_name] = rgt + offset
864 if @parent_issue
864 if @parent_issue
865 move_to_child_of(@parent_issue)
865 move_to_child_of(@parent_issue)
866 end
866 end
867 end
867 end
868 reload
868 reload
869 # delete invalid relations of all descendants
869 # delete invalid relations of all descendants
870 self_and_descendants.each do |issue|
870 self_and_descendants.each do |issue|
871 issue.relations.each do |relation|
871 issue.relations.each do |relation|
872 relation.destroy unless relation.valid?
872 relation.destroy unless relation.valid?
873 end
873 end
874 end
874 end
875 # update former parent
875 # update former parent
876 recalculate_attributes_for(former_parent_id) if former_parent_id
876 recalculate_attributes_for(former_parent_id) if former_parent_id
877 end
877 end
878 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
878 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
879 end
879 end
880
880
881 def update_parent_attributes
881 def update_parent_attributes
882 recalculate_attributes_for(parent_id) if parent_id
882 recalculate_attributes_for(parent_id) if parent_id
883 end
883 end
884
884
885 def recalculate_attributes_for(issue_id)
885 def recalculate_attributes_for(issue_id)
886 if issue_id && p = Issue.find_by_id(issue_id)
886 if issue_id && p = Issue.find_by_id(issue_id)
887 # priority = highest priority of children
887 # priority = highest priority of children
888 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
888 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
889 p.priority = IssuePriority.find_by_position(priority_position)
889 p.priority = IssuePriority.find_by_position(priority_position)
890 end
890 end
891
891
892 # start/due dates = lowest/highest dates of children
892 # start/due dates = lowest/highest dates of children
893 p.start_date = p.children.minimum(:start_date)
893 p.start_date = p.children.minimum(:start_date)
894 p.due_date = p.children.maximum(:due_date)
894 p.due_date = p.children.maximum(:due_date)
895 if p.start_date && p.due_date && p.due_date < p.start_date
895 if p.start_date && p.due_date && p.due_date < p.start_date
896 p.start_date, p.due_date = p.due_date, p.start_date
896 p.start_date, p.due_date = p.due_date, p.start_date
897 end
897 end
898
898
899 # done ratio = weighted average ratio of leaves
899 # done ratio = weighted average ratio of leaves
900 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
900 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
901 leaves_count = p.leaves.count
901 leaves_count = p.leaves.count
902 if leaves_count > 0
902 if leaves_count > 0
903 average = p.leaves.average(:estimated_hours).to_f
903 average = p.leaves.average(:estimated_hours).to_f
904 if average == 0
904 if average == 0
905 average = 1
905 average = 1
906 end
906 end
907 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
907 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
908 progress = done / (average * leaves_count)
908 progress = done / (average * leaves_count)
909 p.done_ratio = progress.round
909 p.done_ratio = progress.round
910 end
910 end
911 end
911 end
912
912
913 # estimate = sum of leaves estimates
913 # estimate = sum of leaves estimates
914 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
914 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
915 p.estimated_hours = nil if p.estimated_hours == 0.0
915 p.estimated_hours = nil if p.estimated_hours == 0.0
916
916
917 # ancestors will be recursively updated
917 # ancestors will be recursively updated
918 p.save(false)
918 p.save(false)
919 end
919 end
920 end
920 end
921
921
922 # Update issues so their versions are not pointing to a
922 # Update issues so their versions are not pointing to a
923 # fixed_version that is not shared with the issue's project
923 # fixed_version that is not shared with the issue's project
924 def self.update_versions(conditions=nil)
924 def self.update_versions(conditions=nil)
925 # Only need to update issues with a fixed_version from
925 # Only need to update issues with a fixed_version from
926 # a different project and that is not systemwide shared
926 # a different project and that is not systemwide shared
927 Issue.scoped(:conditions => conditions).all(
927 Issue.scoped(:conditions => conditions).all(
928 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
928 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
929 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
929 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
930 " AND #{Version.table_name}.sharing <> 'system'",
930 " AND #{Version.table_name}.sharing <> 'system'",
931 :include => [:project, :fixed_version]
931 :include => [:project, :fixed_version]
932 ).each do |issue|
932 ).each do |issue|
933 next if issue.project.nil? || issue.fixed_version.nil?
933 next if issue.project.nil? || issue.fixed_version.nil?
934 unless issue.project.shared_versions.include?(issue.fixed_version)
934 unless issue.project.shared_versions.include?(issue.fixed_version)
935 issue.init_journal(User.current)
935 issue.init_journal(User.current)
936 issue.fixed_version = nil
936 issue.fixed_version = nil
937 issue.save
937 issue.save
938 end
938 end
939 end
939 end
940 end
940 end
941
941
942 # Callback on attachment deletion
942 # Callback on attachment deletion
943 def attachment_added(obj)
943 def attachment_added(obj)
944 if @current_journal && !obj.new_record?
944 if @current_journal && !obj.new_record?
945 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
945 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
946 end
946 end
947 end
947 end
948
948
949 # Callback on attachment deletion
949 # Callback on attachment deletion
950 def attachment_removed(obj)
950 def attachment_removed(obj)
951 journal = init_journal(User.current)
951 journal = init_journal(User.current)
952 journal.details << JournalDetail.new(:property => 'attachment',
952 journal.details << JournalDetail.new(:property => 'attachment',
953 :prop_key => obj.id,
953 :prop_key => obj.id,
954 :old_value => obj.filename)
954 :old_value => obj.filename)
955 journal.save
955 journal.save
956 end
956 end
957
957
958 # Default assignment based on category
958 # Default assignment based on category
959 def default_assign
959 def default_assign
960 if assigned_to.nil? && category && category.assigned_to
960 if assigned_to.nil? && category && category.assigned_to
961 self.assigned_to = category.assigned_to
961 self.assigned_to = category.assigned_to
962 end
962 end
963 end
963 end
964
964
965 # Updates start/due dates of following issues
965 # Updates start/due dates of following issues
966 def reschedule_following_issues
966 def reschedule_following_issues
967 if start_date_changed? || due_date_changed?
967 if start_date_changed? || due_date_changed?
968 relations_from.each do |relation|
968 relations_from.each do |relation|
969 relation.set_issue_to_dates
969 relation.set_issue_to_dates
970 end
970 end
971 end
971 end
972 end
972 end
973
973
974 # Closes duplicates if the issue is being closed
974 # Closes duplicates if the issue is being closed
975 def close_duplicates
975 def close_duplicates
976 if closing?
976 if closing?
977 duplicates.each do |duplicate|
977 duplicates.each do |duplicate|
978 # Reload is need in case the duplicate was updated by a previous duplicate
978 # Reload is need in case the duplicate was updated by a previous duplicate
979 duplicate.reload
979 duplicate.reload
980 # Don't re-close it if it's already closed
980 # Don't re-close it if it's already closed
981 next if duplicate.closed?
981 next if duplicate.closed?
982 # Same user and notes
982 # Same user and notes
983 if @current_journal
983 if @current_journal
984 duplicate.init_journal(@current_journal.user, @current_journal.notes)
984 duplicate.init_journal(@current_journal.user, @current_journal.notes)
985 end
985 end
986 duplicate.update_attribute :status, self.status
986 duplicate.update_attribute :status, self.status
987 end
987 end
988 end
988 end
989 end
989 end
990
990
991 # Saves the changes in a Journal
991 # Saves the changes in a Journal
992 # Called after_save
992 # Called after_save
993 def create_journal
993 def create_journal
994 if @current_journal
994 if @current_journal
995 # attributes changes
995 # attributes changes
996 if @attributes_before_change
996 if @attributes_before_change
997 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
997 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
998 before = @attributes_before_change[c]
998 before = @attributes_before_change[c]
999 after = send(c)
999 after = send(c)
1000 next if before == after || (before.blank? && after.blank?)
1000 next if before == after || (before.blank? && after.blank?)
1001 @current_journal.details << JournalDetail.new(:property => 'attr',
1001 @current_journal.details << JournalDetail.new(:property => 'attr',
1002 :prop_key => c,
1002 :prop_key => c,
1003 :old_value => before,
1003 :old_value => before,
1004 :value => after)
1004 :value => after)
1005 }
1005 }
1006 end
1006 end
1007 if @custom_values_before_change
1007 if @custom_values_before_change
1008 # custom fields changes
1008 # custom fields changes
1009 custom_values.each {|c|
1009 custom_values.each {|c|
1010 before = @custom_values_before_change[c.custom_field_id]
1010 before = @custom_values_before_change[c.custom_field_id]
1011 after = c.value
1011 after = c.value
1012 next if before == after || (before.blank? && after.blank?)
1012 next if before == after || (before.blank? && after.blank?)
1013 @current_journal.details << JournalDetail.new(:property => 'cf',
1013 @current_journal.details << JournalDetail.new(:property => 'cf',
1014 :prop_key => c.custom_field_id,
1014 :prop_key => c.custom_field_id,
1015 :old_value => before,
1015 :old_value => before,
1016 :value => after)
1016 :value => after)
1017 }
1017 }
1018 end
1018 end
1019 @current_journal.save
1019 @current_journal.save
1020 # reset current journal
1020 # reset current journal
1021 init_journal @current_journal.user, @current_journal.notes
1021 init_journal @current_journal.user, @current_journal.notes
1022 end
1022 end
1023 end
1023 end
1024
1024
1025 # Query generator for selecting groups of issue counts for a project
1025 # Query generator for selecting groups of issue counts for a project
1026 # based on specific criteria
1026 # based on specific criteria
1027 #
1027 #
1028 # Options
1028 # Options
1029 # * project - Project to search in.
1029 # * project - Project to search in.
1030 # * field - String. Issue field to key off of in the grouping.
1030 # * field - String. Issue field to key off of in the grouping.
1031 # * joins - String. The table name to join against.
1031 # * joins - String. The table name to join against.
1032 def self.count_and_group_by(options)
1032 def self.count_and_group_by(options)
1033 project = options.delete(:project)
1033 project = options.delete(:project)
1034 select_field = options.delete(:field)
1034 select_field = options.delete(:field)
1035 joins = options.delete(:joins)
1035 joins = options.delete(:joins)
1036
1036
1037 where = "#{Issue.table_name}.#{select_field}=j.id"
1037 where = "#{Issue.table_name}.#{select_field}=j.id"
1038
1038
1039 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1039 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1040 s.is_closed as closed,
1040 s.is_closed as closed,
1041 j.id as #{select_field},
1041 j.id as #{select_field},
1042 count(#{Issue.table_name}.id) as total
1042 count(#{Issue.table_name}.id) as total
1043 from
1043 from
1044 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1044 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1045 where
1045 where
1046 #{Issue.table_name}.status_id=s.id
1046 #{Issue.table_name}.status_id=s.id
1047 and #{where}
1047 and #{where}
1048 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1048 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1049 and #{visible_condition(User.current, :project => project)}
1049 and #{visible_condition(User.current, :project => project)}
1050 group by s.id, s.is_closed, j.id")
1050 group by s.id, s.is_closed, j.id")
1051 end
1051 end
1052 end
1052 end
@@ -1,1204 +1,1213
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :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 def test_create
32 def test_create
33 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
33 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
34 :status_id => 1, :priority => IssuePriority.all.first,
34 :status_id => 1, :priority => IssuePriority.all.first,
35 :subject => 'test_create',
35 :subject => 'test_create',
36 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
36 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
37 assert issue.save
37 assert issue.save
38 issue.reload
38 issue.reload
39 assert_equal 1.5, issue.estimated_hours
39 assert_equal 1.5, issue.estimated_hours
40 end
40 end
41
41
42 def test_create_minimal
42 def test_create_minimal
43 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
43 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
44 :status_id => 1, :priority => IssuePriority.all.first,
44 :status_id => 1, :priority => IssuePriority.all.first,
45 :subject => 'test_create')
45 :subject => 'test_create')
46 assert issue.save
46 assert issue.save
47 assert issue.description.nil?
47 assert issue.description.nil?
48 end
48 end
49
49
50 def test_create_with_required_custom_field
50 def test_create_with_required_custom_field
51 field = IssueCustomField.find_by_name('Database')
51 field = IssueCustomField.find_by_name('Database')
52 field.update_attribute(:is_required, true)
52 field.update_attribute(:is_required, true)
53
53
54 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
54 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
55 :status_id => 1, :subject => 'test_create',
55 :status_id => 1, :subject => 'test_create',
56 :description => 'IssueTest#test_create_with_required_custom_field')
56 :description => 'IssueTest#test_create_with_required_custom_field')
57 assert issue.available_custom_fields.include?(field)
57 assert issue.available_custom_fields.include?(field)
58 # No value for the custom field
58 # No value for the custom field
59 assert !issue.save
59 assert !issue.save
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
61 issue.errors[:custom_values].to_s
61 issue.errors[:custom_values].to_s
62 # Blank value
62 # Blank value
63 issue.custom_field_values = { field.id => '' }
63 issue.custom_field_values = { field.id => '' }
64 assert !issue.save
64 assert !issue.save
65 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
65 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
66 issue.errors[:custom_values].to_s
66 issue.errors[:custom_values].to_s
67 # Invalid value
67 # Invalid value
68 issue.custom_field_values = { field.id => 'SQLServer' }
68 issue.custom_field_values = { field.id => 'SQLServer' }
69 assert !issue.save
69 assert !issue.save
70 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
70 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
71 issue.errors[:custom_values].to_s
71 issue.errors[:custom_values].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_not_nil issue.custom_value_for(1)
330 assert_not_nil issue.custom_value_for(1)
331 assert_equal 'MySQL', issue.custom_value_for(1).value
331 assert_equal 'MySQL', issue.custom_value_for(1).value
332 end
332 end
333
333
334 def test_should_update_issue_with_disabled_tracker
334 def test_should_update_issue_with_disabled_tracker
335 p = Project.find(1)
335 p = Project.find(1)
336 issue = Issue.find(1)
336 issue = Issue.find(1)
337
337
338 p.trackers.delete(issue.tracker)
338 p.trackers.delete(issue.tracker)
339 assert !p.trackers.include?(issue.tracker)
339 assert !p.trackers.include?(issue.tracker)
340
340
341 issue.reload
341 issue.reload
342 issue.subject = 'New subject'
342 issue.subject = 'New subject'
343 assert issue.save
343 assert issue.save
344 end
344 end
345
345
346 def test_should_not_set_a_disabled_tracker
346 def test_should_not_set_a_disabled_tracker
347 p = Project.find(1)
347 p = Project.find(1)
348 p.trackers.delete(Tracker.find(2))
348 p.trackers.delete(Tracker.find(2))
349
349
350 issue = Issue.find(1)
350 issue = Issue.find(1)
351 issue.tracker_id = 2
351 issue.tracker_id = 2
352 issue.subject = 'New subject'
352 issue.subject = 'New subject'
353 assert !issue.save
353 assert !issue.save
354 assert_not_nil issue.errors[:tracker_id]
354 assert_not_nil issue.errors[:tracker_id]
355 end
355 end
356
356
357 def test_category_based_assignment
357 def test_category_based_assignment
358 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
358 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
359 :status_id => 1, :priority => IssuePriority.all.first,
359 :status_id => 1, :priority => IssuePriority.all.first,
360 :subject => 'Assignment test',
360 :subject => 'Assignment test',
361 :description => 'Assignment test', :category_id => 1)
361 :description => 'Assignment test', :category_id => 1)
362 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
362 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
363 end
363 end
364
364
365 def test_new_statuses_allowed_to
365 def test_new_statuses_allowed_to
366 Workflow.delete_all
366 Workflow.delete_all
367
367
368 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 => 2, :author => false, :assignee => false)
369 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 => 3, :author => true, :assignee => false)
370 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 => 4, :author => false, :assignee => true)
371 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
371 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
372 status = IssueStatus.find(1)
372 status = IssueStatus.find(1)
373 role = Role.find(1)
373 role = Role.find(1)
374 tracker = Tracker.find(1)
374 tracker = Tracker.find(1)
375 user = User.find(2)
375 user = User.find(2)
376
376
377 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
377 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
378 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
378 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
379
379
380 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
380 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
381 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
381 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
382
382
383 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
383 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
384 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
384 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
385
385
386 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
386 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
387 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
387 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
388 end
388 end
389
389
390 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
391 admin = User.find(1)
392 issue = Issue.find(1)
393 assert !admin.member_of?(issue.project)
394 expected_statuses = [issue.status] + Workflow.find_all_by_old_status_id(issue.status_id).map(&:new_status).uniq.sort
395
396 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
397 end
398
390 def test_copy
399 def test_copy
391 issue = Issue.new.copy_from(1)
400 issue = Issue.new.copy_from(1)
392 assert issue.copy?
401 assert issue.copy?
393 assert issue.save
402 assert issue.save
394 issue.reload
403 issue.reload
395 orig = Issue.find(1)
404 orig = Issue.find(1)
396 assert_equal orig.subject, issue.subject
405 assert_equal orig.subject, issue.subject
397 assert_equal orig.tracker, issue.tracker
406 assert_equal orig.tracker, issue.tracker
398 assert_equal "125", issue.custom_value_for(2).value
407 assert_equal "125", issue.custom_value_for(2).value
399 end
408 end
400
409
401 def test_copy_should_copy_status
410 def test_copy_should_copy_status
402 orig = Issue.find(8)
411 orig = Issue.find(8)
403 assert orig.status != IssueStatus.default
412 assert orig.status != IssueStatus.default
404
413
405 issue = Issue.new.copy_from(orig)
414 issue = Issue.new.copy_from(orig)
406 assert issue.save
415 assert issue.save
407 issue.reload
416 issue.reload
408 assert_equal orig.status, issue.status
417 assert_equal orig.status, issue.status
409 end
418 end
410
419
411 def test_should_not_call_after_project_change_on_creation
420 def test_should_not_call_after_project_change_on_creation
412 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
421 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Test', :author_id => 1)
413 issue.expects(:after_project_change).never
422 issue.expects(:after_project_change).never
414 issue.save!
423 issue.save!
415 end
424 end
416
425
417 def test_should_not_call_after_project_change_on_update
426 def test_should_not_call_after_project_change_on_update
418 issue = Issue.find(1)
427 issue = Issue.find(1)
419 issue.project = Project.find(1)
428 issue.project = Project.find(1)
420 issue.subject = 'No project change'
429 issue.subject = 'No project change'
421 issue.expects(:after_project_change).never
430 issue.expects(:after_project_change).never
422 issue.save!
431 issue.save!
423 end
432 end
424
433
425 def test_should_call_after_project_change_on_project_change
434 def test_should_call_after_project_change_on_project_change
426 issue = Issue.find(1)
435 issue = Issue.find(1)
427 issue.project = Project.find(2)
436 issue.project = Project.find(2)
428 issue.expects(:after_project_change).once
437 issue.expects(:after_project_change).once
429 issue.save!
438 issue.save!
430 end
439 end
431
440
432 def test_should_close_duplicates
441 def test_should_close_duplicates
433 # Create 3 issues
442 # Create 3 issues
434 project = Project.find(1)
443 project = Project.find(1)
435 issue1 = Issue.generate_for_project!(project)
444 issue1 = Issue.generate_for_project!(project)
436 issue2 = Issue.generate_for_project!(project)
445 issue2 = Issue.generate_for_project!(project)
437 issue3 = Issue.generate_for_project!(project)
446 issue3 = Issue.generate_for_project!(project)
438
447
439 # 2 is a dupe of 1
448 # 2 is a dupe of 1
440 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
449 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
441 # And 3 is a dupe of 2
450 # And 3 is a dupe of 2
442 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
451 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
443 # And 3 is a dupe of 1 (circular duplicates)
452 # And 3 is a dupe of 1 (circular duplicates)
444 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
453 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
445
454
446 assert issue1.reload.duplicates.include?(issue2)
455 assert issue1.reload.duplicates.include?(issue2)
447
456
448 # Closing issue 1
457 # Closing issue 1
449 issue1.init_journal(User.find(:first), "Closing issue1")
458 issue1.init_journal(User.find(:first), "Closing issue1")
450 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
459 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
451 assert issue1.save
460 assert issue1.save
452 # 2 and 3 should be also closed
461 # 2 and 3 should be also closed
453 assert issue2.reload.closed?
462 assert issue2.reload.closed?
454 assert issue3.reload.closed?
463 assert issue3.reload.closed?
455 end
464 end
456
465
457 def test_should_not_close_duplicated_issue
466 def test_should_not_close_duplicated_issue
458 project = Project.find(1)
467 project = Project.find(1)
459 issue1 = Issue.generate_for_project!(project)
468 issue1 = Issue.generate_for_project!(project)
460 issue2 = Issue.generate_for_project!(project)
469 issue2 = Issue.generate_for_project!(project)
461
470
462 # 2 is a dupe of 1
471 # 2 is a dupe of 1
463 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
472 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
464 # 2 is a dup of 1 but 1 is not a duplicate of 2
473 # 2 is a dup of 1 but 1 is not a duplicate of 2
465 assert !issue2.reload.duplicates.include?(issue1)
474 assert !issue2.reload.duplicates.include?(issue1)
466
475
467 # Closing issue 2
476 # Closing issue 2
468 issue2.init_journal(User.find(:first), "Closing issue2")
477 issue2.init_journal(User.find(:first), "Closing issue2")
469 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
478 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
470 assert issue2.save
479 assert issue2.save
471 # 1 should not be also closed
480 # 1 should not be also closed
472 assert !issue1.reload.closed?
481 assert !issue1.reload.closed?
473 end
482 end
474
483
475 def test_assignable_versions
484 def test_assignable_versions
476 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
485 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
477 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
486 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
478 end
487 end
479
488
480 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
489 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
481 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
490 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
482 assert !issue.save
491 assert !issue.save
483 assert_not_nil issue.errors[:fixed_version_id]
492 assert_not_nil issue.errors[:fixed_version_id]
484 end
493 end
485
494
486 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
495 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
487 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
496 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
488 assert !issue.save
497 assert !issue.save
489 assert_not_nil issue.errors[:fixed_version_id]
498 assert_not_nil issue.errors[:fixed_version_id]
490 end
499 end
491
500
492 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
501 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
493 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
502 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
494 assert issue.save
503 assert issue.save
495 end
504 end
496
505
497 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
506 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
498 issue = Issue.find(11)
507 issue = Issue.find(11)
499 assert_equal 'closed', issue.fixed_version.status
508 assert_equal 'closed', issue.fixed_version.status
500 issue.subject = 'Subject changed'
509 issue.subject = 'Subject changed'
501 assert issue.save
510 assert issue.save
502 end
511 end
503
512
504 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
513 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
505 issue = Issue.find(11)
514 issue = Issue.find(11)
506 issue.status_id = 1
515 issue.status_id = 1
507 assert !issue.save
516 assert !issue.save
508 assert_not_nil issue.errors[:base]
517 assert_not_nil issue.errors[:base]
509 end
518 end
510
519
511 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
520 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
512 issue = Issue.find(11)
521 issue = Issue.find(11)
513 issue.status_id = 1
522 issue.status_id = 1
514 issue.fixed_version_id = 3
523 issue.fixed_version_id = 3
515 assert issue.save
524 assert issue.save
516 end
525 end
517
526
518 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
527 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
519 issue = Issue.find(12)
528 issue = Issue.find(12)
520 assert_equal 'locked', issue.fixed_version.status
529 assert_equal 'locked', issue.fixed_version.status
521 issue.status_id = 1
530 issue.status_id = 1
522 assert issue.save
531 assert issue.save
523 end
532 end
524
533
525 def test_move_to_another_project_with_same_category
534 def test_move_to_another_project_with_same_category
526 issue = Issue.find(1)
535 issue = Issue.find(1)
527 issue.project = Project.find(2)
536 issue.project = Project.find(2)
528 assert issue.save
537 assert issue.save
529 issue.reload
538 issue.reload
530 assert_equal 2, issue.project_id
539 assert_equal 2, issue.project_id
531 # Category changes
540 # Category changes
532 assert_equal 4, issue.category_id
541 assert_equal 4, issue.category_id
533 # Make sure time entries were move to the target project
542 # Make sure time entries were move to the target project
534 assert_equal 2, issue.time_entries.first.project_id
543 assert_equal 2, issue.time_entries.first.project_id
535 end
544 end
536
545
537 def test_move_to_another_project_without_same_category
546 def test_move_to_another_project_without_same_category
538 issue = Issue.find(2)
547 issue = Issue.find(2)
539 issue.project = Project.find(2)
548 issue.project = Project.find(2)
540 assert issue.save
549 assert issue.save
541 issue.reload
550 issue.reload
542 assert_equal 2, issue.project_id
551 assert_equal 2, issue.project_id
543 # Category cleared
552 # Category cleared
544 assert_nil issue.category_id
553 assert_nil issue.category_id
545 end
554 end
546
555
547 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
556 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
548 issue = Issue.find(1)
557 issue = Issue.find(1)
549 issue.update_attribute(:fixed_version_id, 1)
558 issue.update_attribute(:fixed_version_id, 1)
550 issue.project = Project.find(2)
559 issue.project = Project.find(2)
551 assert issue.save
560 assert issue.save
552 issue.reload
561 issue.reload
553 assert_equal 2, issue.project_id
562 assert_equal 2, issue.project_id
554 # Cleared fixed_version
563 # Cleared fixed_version
555 assert_equal nil, issue.fixed_version
564 assert_equal nil, issue.fixed_version
556 end
565 end
557
566
558 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
567 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
559 issue = Issue.find(1)
568 issue = Issue.find(1)
560 issue.update_attribute(:fixed_version_id, 4)
569 issue.update_attribute(:fixed_version_id, 4)
561 issue.project = Project.find(5)
570 issue.project = Project.find(5)
562 assert issue.save
571 assert issue.save
563 issue.reload
572 issue.reload
564 assert_equal 5, issue.project_id
573 assert_equal 5, issue.project_id
565 # Keep fixed_version
574 # Keep fixed_version
566 assert_equal 4, issue.fixed_version_id
575 assert_equal 4, issue.fixed_version_id
567 end
576 end
568
577
569 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
578 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
570 issue = Issue.find(1)
579 issue = Issue.find(1)
571 issue.update_attribute(:fixed_version_id, 1)
580 issue.update_attribute(:fixed_version_id, 1)
572 issue.project = Project.find(5)
581 issue.project = Project.find(5)
573 assert issue.save
582 assert issue.save
574 issue.reload
583 issue.reload
575 assert_equal 5, issue.project_id
584 assert_equal 5, issue.project_id
576 # Cleared fixed_version
585 # Cleared fixed_version
577 assert_equal nil, issue.fixed_version
586 assert_equal nil, issue.fixed_version
578 end
587 end
579
588
580 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
589 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
581 issue = Issue.find(1)
590 issue = Issue.find(1)
582 issue.update_attribute(:fixed_version_id, 7)
591 issue.update_attribute(:fixed_version_id, 7)
583 issue.project = Project.find(2)
592 issue.project = Project.find(2)
584 assert issue.save
593 assert issue.save
585 issue.reload
594 issue.reload
586 assert_equal 2, issue.project_id
595 assert_equal 2, issue.project_id
587 # Keep fixed_version
596 # Keep fixed_version
588 assert_equal 7, issue.fixed_version_id
597 assert_equal 7, issue.fixed_version_id
589 end
598 end
590
599
591 def test_move_to_another_project_with_disabled_tracker
600 def test_move_to_another_project_with_disabled_tracker
592 issue = Issue.find(1)
601 issue = Issue.find(1)
593 target = Project.find(2)
602 target = Project.find(2)
594 target.tracker_ids = [3]
603 target.tracker_ids = [3]
595 target.save
604 target.save
596 issue.project = target
605 issue.project = target
597 assert issue.save
606 assert issue.save
598 issue.reload
607 issue.reload
599 assert_equal 2, issue.project_id
608 assert_equal 2, issue.project_id
600 assert_equal 3, issue.tracker_id
609 assert_equal 3, issue.tracker_id
601 end
610 end
602
611
603 def test_copy_to_the_same_project
612 def test_copy_to_the_same_project
604 issue = Issue.find(1)
613 issue = Issue.find(1)
605 copy = issue.copy
614 copy = issue.copy
606 assert_difference 'Issue.count' do
615 assert_difference 'Issue.count' do
607 copy.save!
616 copy.save!
608 end
617 end
609 assert_kind_of Issue, copy
618 assert_kind_of Issue, copy
610 assert_equal issue.project, copy.project
619 assert_equal issue.project, copy.project
611 assert_equal "125", copy.custom_value_for(2).value
620 assert_equal "125", copy.custom_value_for(2).value
612 end
621 end
613
622
614 def test_copy_to_another_project_and_tracker
623 def test_copy_to_another_project_and_tracker
615 issue = Issue.find(1)
624 issue = Issue.find(1)
616 copy = issue.copy(:project_id => 3, :tracker_id => 2)
625 copy = issue.copy(:project_id => 3, :tracker_id => 2)
617 assert_difference 'Issue.count' do
626 assert_difference 'Issue.count' do
618 copy.save!
627 copy.save!
619 end
628 end
620 copy.reload
629 copy.reload
621 assert_kind_of Issue, copy
630 assert_kind_of Issue, copy
622 assert_equal Project.find(3), copy.project
631 assert_equal Project.find(3), copy.project
623 assert_equal Tracker.find(2), copy.tracker
632 assert_equal Tracker.find(2), copy.tracker
624 # Custom field #2 is not associated with target tracker
633 # Custom field #2 is not associated with target tracker
625 assert_nil copy.custom_value_for(2)
634 assert_nil copy.custom_value_for(2)
626 end
635 end
627
636
628 context "#copy" do
637 context "#copy" do
629 setup do
638 setup do
630 @issue = Issue.find(1)
639 @issue = Issue.find(1)
631 end
640 end
632
641
633 should "not create a journal" do
642 should "not create a journal" do
634 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
643 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
635 copy.save!
644 copy.save!
636 assert_equal 0, copy.reload.journals.size
645 assert_equal 0, copy.reload.journals.size
637 end
646 end
638
647
639 should "allow assigned_to changes" do
648 should "allow assigned_to changes" do
640 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
649 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
641 assert_equal 3, copy.assigned_to_id
650 assert_equal 3, copy.assigned_to_id
642 end
651 end
643
652
644 should "allow status changes" do
653 should "allow status changes" do
645 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
654 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
646 assert_equal 2, copy.status_id
655 assert_equal 2, copy.status_id
647 end
656 end
648
657
649 should "allow start date changes" do
658 should "allow start date changes" do
650 date = Date.today
659 date = Date.today
651 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
660 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
652 assert_equal date, copy.start_date
661 assert_equal date, copy.start_date
653 end
662 end
654
663
655 should "allow due date changes" do
664 should "allow due date changes" do
656 date = Date.today
665 date = Date.today
657 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
666 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date)
658 assert_equal date, copy.due_date
667 assert_equal date, copy.due_date
659 end
668 end
660
669
661 should "set current user as author" do
670 should "set current user as author" do
662 User.current = User.find(9)
671 User.current = User.find(9)
663 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
672 copy = @issue.copy(:project_id => 3, :tracker_id => 2)
664 assert_equal User.current, copy.author
673 assert_equal User.current, copy.author
665 end
674 end
666
675
667 should "create a journal with notes" do
676 should "create a journal with notes" do
668 date = Date.today
677 date = Date.today
669 notes = "Notes added when copying"
678 notes = "Notes added when copying"
670 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
679 copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date)
671 copy.init_journal(User.current, notes)
680 copy.init_journal(User.current, notes)
672 copy.save!
681 copy.save!
673
682
674 assert_equal 1, copy.journals.size
683 assert_equal 1, copy.journals.size
675 journal = copy.journals.first
684 journal = copy.journals.first
676 assert_equal 0, journal.details.size
685 assert_equal 0, journal.details.size
677 assert_equal notes, journal.notes
686 assert_equal notes, journal.notes
678 end
687 end
679 end
688 end
680
689
681 def test_recipients_should_include_previous_assignee
690 def test_recipients_should_include_previous_assignee
682 user = User.find(3)
691 user = User.find(3)
683 user.members.update_all ["mail_notification = ?", false]
692 user.members.update_all ["mail_notification = ?", false]
684 user.update_attribute :mail_notification, 'only_assigned'
693 user.update_attribute :mail_notification, 'only_assigned'
685
694
686 issue = Issue.find(2)
695 issue = Issue.find(2)
687 issue.assigned_to = nil
696 issue.assigned_to = nil
688 assert_include user.mail, issue.recipients
697 assert_include user.mail, issue.recipients
689 issue.save!
698 issue.save!
690 assert !issue.recipients.include?(user.mail)
699 assert !issue.recipients.include?(user.mail)
691 end
700 end
692
701
693 def test_recipients_should_not_include_users_that_cannot_view_the_issue
702 def test_recipients_should_not_include_users_that_cannot_view_the_issue
694 issue = Issue.find(12)
703 issue = Issue.find(12)
695 assert issue.recipients.include?(issue.author.mail)
704 assert issue.recipients.include?(issue.author.mail)
696 # copy the issue to a private project
705 # copy the issue to a private project
697 copy = issue.copy(:project_id => 5, :tracker_id => 2)
706 copy = issue.copy(:project_id => 5, :tracker_id => 2)
698 # author is not a member of project anymore
707 # author is not a member of project anymore
699 assert !copy.recipients.include?(copy.author.mail)
708 assert !copy.recipients.include?(copy.author.mail)
700 end
709 end
701
710
702 def test_recipients_should_include_the_assigned_group_members
711 def test_recipients_should_include_the_assigned_group_members
703 group_member = User.generate_with_protected!
712 group_member = User.generate_with_protected!
704 group = Group.generate!
713 group = Group.generate!
705 group.users << group_member
714 group.users << group_member
706
715
707 issue = Issue.find(12)
716 issue = Issue.find(12)
708 issue.assigned_to = group
717 issue.assigned_to = group
709 assert issue.recipients.include?(group_member.mail)
718 assert issue.recipients.include?(group_member.mail)
710 end
719 end
711
720
712 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
721 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
713 user = User.find(3)
722 user = User.find(3)
714 issue = Issue.find(9)
723 issue = Issue.find(9)
715 Watcher.create!(:user => user, :watchable => issue)
724 Watcher.create!(:user => user, :watchable => issue)
716 assert issue.watched_by?(user)
725 assert issue.watched_by?(user)
717 assert !issue.watcher_recipients.include?(user.mail)
726 assert !issue.watcher_recipients.include?(user.mail)
718 end
727 end
719
728
720 def test_issue_destroy
729 def test_issue_destroy
721 Issue.find(1).destroy
730 Issue.find(1).destroy
722 assert_nil Issue.find_by_id(1)
731 assert_nil Issue.find_by_id(1)
723 assert_nil TimeEntry.find_by_issue_id(1)
732 assert_nil TimeEntry.find_by_issue_id(1)
724 end
733 end
725
734
726 def test_blocked
735 def test_blocked
727 blocked_issue = Issue.find(9)
736 blocked_issue = Issue.find(9)
728 blocking_issue = Issue.find(10)
737 blocking_issue = Issue.find(10)
729
738
730 assert blocked_issue.blocked?
739 assert blocked_issue.blocked?
731 assert !blocking_issue.blocked?
740 assert !blocking_issue.blocked?
732 end
741 end
733
742
734 def test_blocked_issues_dont_allow_closed_statuses
743 def test_blocked_issues_dont_allow_closed_statuses
735 blocked_issue = Issue.find(9)
744 blocked_issue = Issue.find(9)
736
745
737 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
746 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
738 assert !allowed_statuses.empty?
747 assert !allowed_statuses.empty?
739 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
748 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
740 assert closed_statuses.empty?
749 assert closed_statuses.empty?
741 end
750 end
742
751
743 def test_unblocked_issues_allow_closed_statuses
752 def test_unblocked_issues_allow_closed_statuses
744 blocking_issue = Issue.find(10)
753 blocking_issue = Issue.find(10)
745
754
746 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
755 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
747 assert !allowed_statuses.empty?
756 assert !allowed_statuses.empty?
748 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
757 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
749 assert !closed_statuses.empty?
758 assert !closed_statuses.empty?
750 end
759 end
751
760
752 def test_rescheduling_an_issue_should_reschedule_following_issue
761 def test_rescheduling_an_issue_should_reschedule_following_issue
753 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 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)
754 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 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)
755 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
764 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
756 assert_equal issue1.due_date + 1, issue2.reload.start_date
765 assert_equal issue1.due_date + 1, issue2.reload.start_date
757
766
758 issue1.due_date = Date.today + 5
767 issue1.due_date = Date.today + 5
759 issue1.save!
768 issue1.save!
760 assert_equal issue1.due_date + 1, issue2.reload.start_date
769 assert_equal issue1.due_date + 1, issue2.reload.start_date
761 end
770 end
762
771
763 def test_overdue
772 def test_overdue
764 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
773 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
765 assert !Issue.new(:due_date => Date.today).overdue?
774 assert !Issue.new(:due_date => Date.today).overdue?
766 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
775 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
767 assert !Issue.new(:due_date => nil).overdue?
776 assert !Issue.new(:due_date => nil).overdue?
768 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
777 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
769 end
778 end
770
779
771 context "#behind_schedule?" do
780 context "#behind_schedule?" do
772 should "be false if the issue has no start_date" do
781 should "be false if the issue has no start_date" do
773 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
782 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
774 end
783 end
775
784
776 should "be false if the issue has no end_date" do
785 should "be false if the issue has no end_date" do
777 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
786 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
778 end
787 end
779
788
780 should "be false if the issue has more done than it's calendar time" do
789 should "be false if the issue has more done than it's calendar time" do
781 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
790 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
782 end
791 end
783
792
784 should "be true if the issue hasn't been started at all" do
793 should "be true if the issue hasn't been started at all" do
785 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
794 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
786 end
795 end
787
796
788 should "be true if the issue has used more calendar time than it's done ratio" do
797 should "be true if the issue has used more calendar time than it's done ratio" do
789 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
798 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
790 end
799 end
791 end
800 end
792
801
793 context "#assignable_users" do
802 context "#assignable_users" do
794 should "be Users" do
803 should "be Users" do
795 assert_kind_of User, Issue.find(1).assignable_users.first
804 assert_kind_of User, Issue.find(1).assignable_users.first
796 end
805 end
797
806
798 should "include the issue author" do
807 should "include the issue author" do
799 project = Project.find(1)
808 project = Project.find(1)
800 non_project_member = User.generate!
809 non_project_member = User.generate!
801 issue = Issue.generate_for_project!(project, :author => non_project_member)
810 issue = Issue.generate_for_project!(project, :author => non_project_member)
802
811
803 assert issue.assignable_users.include?(non_project_member)
812 assert issue.assignable_users.include?(non_project_member)
804 end
813 end
805
814
806 should "include the current assignee" do
815 should "include the current assignee" do
807 project = Project.find(1)
816 project = Project.find(1)
808 user = User.generate!
817 user = User.generate!
809 issue = Issue.generate_for_project!(project, :assigned_to => user)
818 issue = Issue.generate_for_project!(project, :assigned_to => user)
810 user.lock!
819 user.lock!
811
820
812 assert Issue.find(issue.id).assignable_users.include?(user)
821 assert Issue.find(issue.id).assignable_users.include?(user)
813 end
822 end
814
823
815 should "not show the issue author twice" do
824 should "not show the issue author twice" do
816 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
825 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
817 assert_equal 2, assignable_user_ids.length
826 assert_equal 2, assignable_user_ids.length
818
827
819 assignable_user_ids.each do |user_id|
828 assignable_user_ids.each do |user_id|
820 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
829 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
821 end
830 end
822 end
831 end
823
832
824 context "with issue_group_assignment" do
833 context "with issue_group_assignment" do
825 should "include groups" do
834 should "include groups" do
826 issue = Issue.new(:project => Project.find(2))
835 issue = Issue.new(:project => Project.find(2))
827
836
828 with_settings :issue_group_assignment => '1' do
837 with_settings :issue_group_assignment => '1' do
829 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
838 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
830 assert issue.assignable_users.include?(Group.find(11))
839 assert issue.assignable_users.include?(Group.find(11))
831 end
840 end
832 end
841 end
833 end
842 end
834
843
835 context "without issue_group_assignment" do
844 context "without issue_group_assignment" do
836 should "not include groups" do
845 should "not include groups" do
837 issue = Issue.new(:project => Project.find(2))
846 issue = Issue.new(:project => Project.find(2))
838
847
839 with_settings :issue_group_assignment => '0' do
848 with_settings :issue_group_assignment => '0' do
840 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
849 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
841 assert !issue.assignable_users.include?(Group.find(11))
850 assert !issue.assignable_users.include?(Group.find(11))
842 end
851 end
843 end
852 end
844 end
853 end
845 end
854 end
846
855
847 def test_create_should_send_email_notification
856 def test_create_should_send_email_notification
848 ActionMailer::Base.deliveries.clear
857 ActionMailer::Base.deliveries.clear
849 issue = Issue.new(:project_id => 1, :tracker_id => 1,
858 issue = Issue.new(:project_id => 1, :tracker_id => 1,
850 :author_id => 3, :status_id => 1,
859 :author_id => 3, :status_id => 1,
851 :priority => IssuePriority.all.first,
860 :priority => IssuePriority.all.first,
852 :subject => 'test_create', :estimated_hours => '1:30')
861 :subject => 'test_create', :estimated_hours => '1:30')
853
862
854 assert issue.save
863 assert issue.save
855 assert_equal 1, ActionMailer::Base.deliveries.size
864 assert_equal 1, ActionMailer::Base.deliveries.size
856 end
865 end
857
866
858 def test_stale_issue_should_not_send_email_notification
867 def test_stale_issue_should_not_send_email_notification
859 ActionMailer::Base.deliveries.clear
868 ActionMailer::Base.deliveries.clear
860 issue = Issue.find(1)
869 issue = Issue.find(1)
861 stale = Issue.find(1)
870 stale = Issue.find(1)
862
871
863 issue.init_journal(User.find(1))
872 issue.init_journal(User.find(1))
864 issue.subject = 'Subjet update'
873 issue.subject = 'Subjet update'
865 assert issue.save
874 assert issue.save
866 assert_equal 1, ActionMailer::Base.deliveries.size
875 assert_equal 1, ActionMailer::Base.deliveries.size
867 ActionMailer::Base.deliveries.clear
876 ActionMailer::Base.deliveries.clear
868
877
869 stale.init_journal(User.find(1))
878 stale.init_journal(User.find(1))
870 stale.subject = 'Another subjet update'
879 stale.subject = 'Another subjet update'
871 assert_raise ActiveRecord::StaleObjectError do
880 assert_raise ActiveRecord::StaleObjectError do
872 stale.save
881 stale.save
873 end
882 end
874 assert ActionMailer::Base.deliveries.empty?
883 assert ActionMailer::Base.deliveries.empty?
875 end
884 end
876
885
877 def test_journalized_description
886 def test_journalized_description
878 IssueCustomField.delete_all
887 IssueCustomField.delete_all
879
888
880 i = Issue.first
889 i = Issue.first
881 old_description = i.description
890 old_description = i.description
882 new_description = "This is the new description"
891 new_description = "This is the new description"
883
892
884 i.init_journal(User.find(2))
893 i.init_journal(User.find(2))
885 i.description = new_description
894 i.description = new_description
886 assert_difference 'Journal.count', 1 do
895 assert_difference 'Journal.count', 1 do
887 assert_difference 'JournalDetail.count', 1 do
896 assert_difference 'JournalDetail.count', 1 do
888 i.save!
897 i.save!
889 end
898 end
890 end
899 end
891
900
892 detail = JournalDetail.first(:order => 'id DESC')
901 detail = JournalDetail.first(:order => 'id DESC')
893 assert_equal i, detail.journal.journalized
902 assert_equal i, detail.journal.journalized
894 assert_equal 'attr', detail.property
903 assert_equal 'attr', detail.property
895 assert_equal 'description', detail.prop_key
904 assert_equal 'description', detail.prop_key
896 assert_equal old_description, detail.old_value
905 assert_equal old_description, detail.old_value
897 assert_equal new_description, detail.value
906 assert_equal new_description, detail.value
898 end
907 end
899
908
900 def test_blank_descriptions_should_not_be_journalized
909 def test_blank_descriptions_should_not_be_journalized
901 IssueCustomField.delete_all
910 IssueCustomField.delete_all
902 Issue.update_all("description = NULL", "id=1")
911 Issue.update_all("description = NULL", "id=1")
903
912
904 i = Issue.find(1)
913 i = Issue.find(1)
905 i.init_journal(User.find(2))
914 i.init_journal(User.find(2))
906 i.subject = "blank description"
915 i.subject = "blank description"
907 i.description = "\r\n"
916 i.description = "\r\n"
908
917
909 assert_difference 'Journal.count', 1 do
918 assert_difference 'Journal.count', 1 do
910 assert_difference 'JournalDetail.count', 1 do
919 assert_difference 'JournalDetail.count', 1 do
911 i.save!
920 i.save!
912 end
921 end
913 end
922 end
914 end
923 end
915
924
916 def test_description_eol_should_be_normalized
925 def test_description_eol_should_be_normalized
917 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
926 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
918 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
927 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
919 end
928 end
920
929
921 def test_saving_twice_should_not_duplicate_journal_details
930 def test_saving_twice_should_not_duplicate_journal_details
922 i = Issue.find(:first)
931 i = Issue.find(:first)
923 i.init_journal(User.find(2), 'Some notes')
932 i.init_journal(User.find(2), 'Some notes')
924 # initial changes
933 # initial changes
925 i.subject = 'New subject'
934 i.subject = 'New subject'
926 i.done_ratio = i.done_ratio + 10
935 i.done_ratio = i.done_ratio + 10
927 assert_difference 'Journal.count' do
936 assert_difference 'Journal.count' do
928 assert i.save
937 assert i.save
929 end
938 end
930 # 1 more change
939 # 1 more change
931 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
940 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
932 assert_no_difference 'Journal.count' do
941 assert_no_difference 'Journal.count' do
933 assert_difference 'JournalDetail.count', 1 do
942 assert_difference 'JournalDetail.count', 1 do
934 i.save
943 i.save
935 end
944 end
936 end
945 end
937 # no more change
946 # no more change
938 assert_no_difference 'Journal.count' do
947 assert_no_difference 'Journal.count' do
939 assert_no_difference 'JournalDetail.count' do
948 assert_no_difference 'JournalDetail.count' do
940 i.save
949 i.save
941 end
950 end
942 end
951 end
943 end
952 end
944
953
945 def test_all_dependent_issues
954 def test_all_dependent_issues
946 IssueRelation.delete_all
955 IssueRelation.delete_all
947 assert IssueRelation.create!(:issue_from => Issue.find(1),
956 assert IssueRelation.create!(:issue_from => Issue.find(1),
948 :issue_to => Issue.find(2),
957 :issue_to => Issue.find(2),
949 :relation_type => IssueRelation::TYPE_PRECEDES)
958 :relation_type => IssueRelation::TYPE_PRECEDES)
950 assert IssueRelation.create!(:issue_from => Issue.find(2),
959 assert IssueRelation.create!(:issue_from => Issue.find(2),
951 :issue_to => Issue.find(3),
960 :issue_to => Issue.find(3),
952 :relation_type => IssueRelation::TYPE_PRECEDES)
961 :relation_type => IssueRelation::TYPE_PRECEDES)
953 assert IssueRelation.create!(:issue_from => Issue.find(3),
962 assert IssueRelation.create!(:issue_from => Issue.find(3),
954 :issue_to => Issue.find(8),
963 :issue_to => Issue.find(8),
955 :relation_type => IssueRelation::TYPE_PRECEDES)
964 :relation_type => IssueRelation::TYPE_PRECEDES)
956
965
957 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
966 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
958 end
967 end
959
968
960 def test_all_dependent_issues_with_persistent_circular_dependency
969 def test_all_dependent_issues_with_persistent_circular_dependency
961 IssueRelation.delete_all
970 IssueRelation.delete_all
962 assert IssueRelation.create!(:issue_from => Issue.find(1),
971 assert IssueRelation.create!(:issue_from => Issue.find(1),
963 :issue_to => Issue.find(2),
972 :issue_to => Issue.find(2),
964 :relation_type => IssueRelation::TYPE_PRECEDES)
973 :relation_type => IssueRelation::TYPE_PRECEDES)
965 assert IssueRelation.create!(:issue_from => Issue.find(2),
974 assert IssueRelation.create!(:issue_from => Issue.find(2),
966 :issue_to => Issue.find(3),
975 :issue_to => Issue.find(3),
967 :relation_type => IssueRelation::TYPE_PRECEDES)
976 :relation_type => IssueRelation::TYPE_PRECEDES)
968 # Validation skipping
977 # Validation skipping
969 assert IssueRelation.new(:issue_from => Issue.find(3),
978 assert IssueRelation.new(:issue_from => Issue.find(3),
970 :issue_to => Issue.find(1),
979 :issue_to => Issue.find(1),
971 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
980 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
972
981
973 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
982 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
974 end
983 end
975
984
976 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
985 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
977 IssueRelation.delete_all
986 IssueRelation.delete_all
978 assert IssueRelation.create!(:issue_from => Issue.find(1),
987 assert IssueRelation.create!(:issue_from => Issue.find(1),
979 :issue_to => Issue.find(2),
988 :issue_to => Issue.find(2),
980 :relation_type => IssueRelation::TYPE_RELATES)
989 :relation_type => IssueRelation::TYPE_RELATES)
981 assert IssueRelation.create!(:issue_from => Issue.find(2),
990 assert IssueRelation.create!(:issue_from => Issue.find(2),
982 :issue_to => Issue.find(3),
991 :issue_to => Issue.find(3),
983 :relation_type => IssueRelation::TYPE_RELATES)
992 :relation_type => IssueRelation::TYPE_RELATES)
984 assert IssueRelation.create!(:issue_from => Issue.find(3),
993 assert IssueRelation.create!(:issue_from => Issue.find(3),
985 :issue_to => Issue.find(8),
994 :issue_to => Issue.find(8),
986 :relation_type => IssueRelation::TYPE_RELATES)
995 :relation_type => IssueRelation::TYPE_RELATES)
987 # Validation skipping
996 # Validation skipping
988 assert IssueRelation.new(:issue_from => Issue.find(8),
997 assert IssueRelation.new(:issue_from => Issue.find(8),
989 :issue_to => Issue.find(2),
998 :issue_to => Issue.find(2),
990 :relation_type => IssueRelation::TYPE_RELATES).save(false)
999 :relation_type => IssueRelation::TYPE_RELATES).save(false)
991 assert IssueRelation.new(:issue_from => Issue.find(3),
1000 assert IssueRelation.new(:issue_from => Issue.find(3),
992 :issue_to => Issue.find(1),
1001 :issue_to => Issue.find(1),
993 :relation_type => IssueRelation::TYPE_RELATES).save(false)
1002 :relation_type => IssueRelation::TYPE_RELATES).save(false)
994
1003
995 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1004 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
996 end
1005 end
997
1006
998 context "#done_ratio" do
1007 context "#done_ratio" do
999 setup do
1008 setup do
1000 @issue = Issue.find(1)
1009 @issue = Issue.find(1)
1001 @issue_status = IssueStatus.find(1)
1010 @issue_status = IssueStatus.find(1)
1002 @issue_status.update_attribute(:default_done_ratio, 50)
1011 @issue_status.update_attribute(:default_done_ratio, 50)
1003 @issue2 = Issue.find(2)
1012 @issue2 = Issue.find(2)
1004 @issue_status2 = IssueStatus.find(2)
1013 @issue_status2 = IssueStatus.find(2)
1005 @issue_status2.update_attribute(:default_done_ratio, 0)
1014 @issue_status2.update_attribute(:default_done_ratio, 0)
1006 end
1015 end
1007
1016
1008 teardown do
1017 teardown do
1009 Setting.issue_done_ratio = 'issue_field'
1018 Setting.issue_done_ratio = 'issue_field'
1010 end
1019 end
1011
1020
1012 context "with Setting.issue_done_ratio using the issue_field" do
1021 context "with Setting.issue_done_ratio using the issue_field" do
1013 setup do
1022 setup do
1014 Setting.issue_done_ratio = 'issue_field'
1023 Setting.issue_done_ratio = 'issue_field'
1015 end
1024 end
1016
1025
1017 should "read the issue's field" do
1026 should "read the issue's field" do
1018 assert_equal 0, @issue.done_ratio
1027 assert_equal 0, @issue.done_ratio
1019 assert_equal 30, @issue2.done_ratio
1028 assert_equal 30, @issue2.done_ratio
1020 end
1029 end
1021 end
1030 end
1022
1031
1023 context "with Setting.issue_done_ratio using the issue_status" do
1032 context "with Setting.issue_done_ratio using the issue_status" do
1024 setup do
1033 setup do
1025 Setting.issue_done_ratio = 'issue_status'
1034 Setting.issue_done_ratio = 'issue_status'
1026 end
1035 end
1027
1036
1028 should "read the Issue Status's default done ratio" do
1037 should "read the Issue Status's default done ratio" do
1029 assert_equal 50, @issue.done_ratio
1038 assert_equal 50, @issue.done_ratio
1030 assert_equal 0, @issue2.done_ratio
1039 assert_equal 0, @issue2.done_ratio
1031 end
1040 end
1032 end
1041 end
1033 end
1042 end
1034
1043
1035 context "#update_done_ratio_from_issue_status" do
1044 context "#update_done_ratio_from_issue_status" do
1036 setup do
1045 setup do
1037 @issue = Issue.find(1)
1046 @issue = Issue.find(1)
1038 @issue_status = IssueStatus.find(1)
1047 @issue_status = IssueStatus.find(1)
1039 @issue_status.update_attribute(:default_done_ratio, 50)
1048 @issue_status.update_attribute(:default_done_ratio, 50)
1040 @issue2 = Issue.find(2)
1049 @issue2 = Issue.find(2)
1041 @issue_status2 = IssueStatus.find(2)
1050 @issue_status2 = IssueStatus.find(2)
1042 @issue_status2.update_attribute(:default_done_ratio, 0)
1051 @issue_status2.update_attribute(:default_done_ratio, 0)
1043 end
1052 end
1044
1053
1045 context "with Setting.issue_done_ratio using the issue_field" do
1054 context "with Setting.issue_done_ratio using the issue_field" do
1046 setup do
1055 setup do
1047 Setting.issue_done_ratio = 'issue_field'
1056 Setting.issue_done_ratio = 'issue_field'
1048 end
1057 end
1049
1058
1050 should "not change the issue" do
1059 should "not change the issue" do
1051 @issue.update_done_ratio_from_issue_status
1060 @issue.update_done_ratio_from_issue_status
1052 @issue2.update_done_ratio_from_issue_status
1061 @issue2.update_done_ratio_from_issue_status
1053
1062
1054 assert_equal 0, @issue.read_attribute(:done_ratio)
1063 assert_equal 0, @issue.read_attribute(:done_ratio)
1055 assert_equal 30, @issue2.read_attribute(:done_ratio)
1064 assert_equal 30, @issue2.read_attribute(:done_ratio)
1056 end
1065 end
1057 end
1066 end
1058
1067
1059 context "with Setting.issue_done_ratio using the issue_status" do
1068 context "with Setting.issue_done_ratio using the issue_status" do
1060 setup do
1069 setup do
1061 Setting.issue_done_ratio = 'issue_status'
1070 Setting.issue_done_ratio = 'issue_status'
1062 end
1071 end
1063
1072
1064 should "change the issue's done ratio" do
1073 should "change the issue's done ratio" do
1065 @issue.update_done_ratio_from_issue_status
1074 @issue.update_done_ratio_from_issue_status
1066 @issue2.update_done_ratio_from_issue_status
1075 @issue2.update_done_ratio_from_issue_status
1067
1076
1068 assert_equal 50, @issue.read_attribute(:done_ratio)
1077 assert_equal 50, @issue.read_attribute(:done_ratio)
1069 assert_equal 0, @issue2.read_attribute(:done_ratio)
1078 assert_equal 0, @issue2.read_attribute(:done_ratio)
1070 end
1079 end
1071 end
1080 end
1072 end
1081 end
1073
1082
1074 test "#by_tracker" do
1083 test "#by_tracker" do
1075 User.current = User.anonymous
1084 User.current = User.anonymous
1076 groups = Issue.by_tracker(Project.find(1))
1085 groups = Issue.by_tracker(Project.find(1))
1077 assert_equal 3, groups.size
1086 assert_equal 3, groups.size
1078 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1087 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1079 end
1088 end
1080
1089
1081 test "#by_version" do
1090 test "#by_version" do
1082 User.current = User.anonymous
1091 User.current = User.anonymous
1083 groups = Issue.by_version(Project.find(1))
1092 groups = Issue.by_version(Project.find(1))
1084 assert_equal 3, groups.size
1093 assert_equal 3, groups.size
1085 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1094 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1086 end
1095 end
1087
1096
1088 test "#by_priority" do
1097 test "#by_priority" do
1089 User.current = User.anonymous
1098 User.current = User.anonymous
1090 groups = Issue.by_priority(Project.find(1))
1099 groups = Issue.by_priority(Project.find(1))
1091 assert_equal 4, groups.size
1100 assert_equal 4, groups.size
1092 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1101 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1093 end
1102 end
1094
1103
1095 test "#by_category" do
1104 test "#by_category" do
1096 User.current = User.anonymous
1105 User.current = User.anonymous
1097 groups = Issue.by_category(Project.find(1))
1106 groups = Issue.by_category(Project.find(1))
1098 assert_equal 2, groups.size
1107 assert_equal 2, groups.size
1099 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1108 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1100 end
1109 end
1101
1110
1102 test "#by_assigned_to" do
1111 test "#by_assigned_to" do
1103 User.current = User.anonymous
1112 User.current = User.anonymous
1104 groups = Issue.by_assigned_to(Project.find(1))
1113 groups = Issue.by_assigned_to(Project.find(1))
1105 assert_equal 2, groups.size
1114 assert_equal 2, groups.size
1106 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1115 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1107 end
1116 end
1108
1117
1109 test "#by_author" do
1118 test "#by_author" do
1110 User.current = User.anonymous
1119 User.current = User.anonymous
1111 groups = Issue.by_author(Project.find(1))
1120 groups = Issue.by_author(Project.find(1))
1112 assert_equal 4, groups.size
1121 assert_equal 4, groups.size
1113 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1122 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1114 end
1123 end
1115
1124
1116 test "#by_subproject" do
1125 test "#by_subproject" do
1117 User.current = User.anonymous
1126 User.current = User.anonymous
1118 groups = Issue.by_subproject(Project.find(1))
1127 groups = Issue.by_subproject(Project.find(1))
1119 # Private descendant not visible
1128 # Private descendant not visible
1120 assert_equal 1, groups.size
1129 assert_equal 1, groups.size
1121 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1130 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1122 end
1131 end
1123
1132
1124 context ".allowed_target_projects_on_move" do
1133 context ".allowed_target_projects_on_move" do
1125 should "return all active projects for admin users" do
1134 should "return all active projects for admin users" do
1126 User.current = User.find(1)
1135 User.current = User.find(1)
1127 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1136 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1128 end
1137 end
1129
1138
1130 should "return allowed projects for non admin users" do
1139 should "return allowed projects for non admin users" do
1131 User.current = User.find(2)
1140 User.current = User.find(2)
1132 Role.non_member.remove_permission! :move_issues
1141 Role.non_member.remove_permission! :move_issues
1133 assert_equal 3, Issue.allowed_target_projects_on_move.size
1142 assert_equal 3, Issue.allowed_target_projects_on_move.size
1134
1143
1135 Role.non_member.add_permission! :move_issues
1144 Role.non_member.add_permission! :move_issues
1136 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1145 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1137 end
1146 end
1138 end
1147 end
1139
1148
1140 def test_recently_updated_with_limit_scopes
1149 def test_recently_updated_with_limit_scopes
1141 #should return the last updated issue
1150 #should return the last updated issue
1142 assert_equal 1, Issue.recently_updated.with_limit(1).length
1151 assert_equal 1, Issue.recently_updated.with_limit(1).length
1143 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1152 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1144 end
1153 end
1145
1154
1146 def test_on_active_projects_scope
1155 def test_on_active_projects_scope
1147 assert Project.find(2).archive
1156 assert Project.find(2).archive
1148
1157
1149 before = Issue.on_active_project.length
1158 before = Issue.on_active_project.length
1150 # test inclusion to results
1159 # test inclusion to results
1151 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1160 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1152 assert_equal before + 1, Issue.on_active_project.length
1161 assert_equal before + 1, Issue.on_active_project.length
1153
1162
1154 # Move to an archived project
1163 # Move to an archived project
1155 issue.project = Project.find(2)
1164 issue.project = Project.find(2)
1156 assert issue.save
1165 assert issue.save
1157 assert_equal before, Issue.on_active_project.length
1166 assert_equal before, Issue.on_active_project.length
1158 end
1167 end
1159
1168
1160 context "Issue#recipients" do
1169 context "Issue#recipients" do
1161 setup do
1170 setup do
1162 @project = Project.find(1)
1171 @project = Project.find(1)
1163 @author = User.generate_with_protected!
1172 @author = User.generate_with_protected!
1164 @assignee = User.generate_with_protected!
1173 @assignee = User.generate_with_protected!
1165 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1174 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1166 end
1175 end
1167
1176
1168 should "include project recipients" do
1177 should "include project recipients" do
1169 assert @project.recipients.present?
1178 assert @project.recipients.present?
1170 @project.recipients.each do |project_recipient|
1179 @project.recipients.each do |project_recipient|
1171 assert @issue.recipients.include?(project_recipient)
1180 assert @issue.recipients.include?(project_recipient)
1172 end
1181 end
1173 end
1182 end
1174
1183
1175 should "include the author if the author is active" do
1184 should "include the author if the author is active" do
1176 assert @issue.author, "No author set for Issue"
1185 assert @issue.author, "No author set for Issue"
1177 assert @issue.recipients.include?(@issue.author.mail)
1186 assert @issue.recipients.include?(@issue.author.mail)
1178 end
1187 end
1179
1188
1180 should "include the assigned to user if the assigned to user is active" do
1189 should "include the assigned to user if the assigned to user is active" do
1181 assert @issue.assigned_to, "No assigned_to set for Issue"
1190 assert @issue.assigned_to, "No assigned_to set for Issue"
1182 assert @issue.recipients.include?(@issue.assigned_to.mail)
1191 assert @issue.recipients.include?(@issue.assigned_to.mail)
1183 end
1192 end
1184
1193
1185 should "not include users who opt out of all email" do
1194 should "not include users who opt out of all email" do
1186 @author.update_attribute(:mail_notification, :none)
1195 @author.update_attribute(:mail_notification, :none)
1187
1196
1188 assert !@issue.recipients.include?(@issue.author.mail)
1197 assert !@issue.recipients.include?(@issue.author.mail)
1189 end
1198 end
1190
1199
1191 should "not include the issue author if they are only notified of assigned issues" do
1200 should "not include the issue author if they are only notified of assigned issues" do
1192 @author.update_attribute(:mail_notification, :only_assigned)
1201 @author.update_attribute(:mail_notification, :only_assigned)
1193
1202
1194 assert !@issue.recipients.include?(@issue.author.mail)
1203 assert !@issue.recipients.include?(@issue.author.mail)
1195 end
1204 end
1196
1205
1197 should "not include the assigned user if they are only notified of owned issues" do
1206 should "not include the assigned user if they are only notified of owned issues" do
1198 @assignee.update_attribute(:mail_notification, :only_owner)
1207 @assignee.update_attribute(:mail_notification, :only_owner)
1199
1208
1200 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1209 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1201 end
1210 end
1202
1211
1203 end
1212 end
1204 end
1213 end
General Comments 0
You need to be logged in to leave comments. Login now