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