##// END OF EJS Templates
Additional status transitions for assignees do not work if assigned to a group (#14447)....
Jean-Philippe Lang -
r11826:474453d2b0af
parent child
Show More
@@ -1,1553 +1,1557
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 belongs_to :project
23 belongs_to :project
24 belongs_to :tracker
24 belongs_to :tracker
25 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
25 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
26 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
26 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
27 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
27 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
28 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
28 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
29 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
29 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
30 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
30 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
31
31
32 has_many :journals, :as => :journalized, :dependent => :destroy
32 has_many :journals, :as => :journalized, :dependent => :destroy
33 has_many :visible_journals,
33 has_many :visible_journals,
34 :class_name => 'Journal',
34 :class_name => 'Journal',
35 :as => :journalized,
35 :as => :journalized,
36 :conditions => Proc.new {
36 :conditions => Proc.new {
37 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
37 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
38 },
38 },
39 :readonly => true
39 :readonly => true
40
40
41 has_many :time_entries, :dependent => :delete_all
41 has_many :time_entries, :dependent => :delete_all
42 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
42 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
43
43
44 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
45 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
46
46
47 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
47 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
48 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
48 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
49 acts_as_customizable
49 acts_as_customizable
50 acts_as_watchable
50 acts_as_watchable
51 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
51 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
52 :include => [:project, :visible_journals],
52 :include => [:project, :visible_journals],
53 # sort by id so that limited eager loading doesn't break with postgresql
53 # sort by id so that limited eager loading doesn't break with postgresql
54 :order_column => "#{table_name}.id"
54 :order_column => "#{table_name}.id"
55 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
55 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
56 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
56 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
57 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
57 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
58
58
59 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
59 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
60 :author_key => :author_id
60 :author_key => :author_id
61
61
62 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
62 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
63
63
64 attr_reader :current_journal
64 attr_reader :current_journal
65 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
65 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
66
66
67 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
67 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
68
68
69 validates_length_of :subject, :maximum => 255
69 validates_length_of :subject, :maximum => 255
70 validates_inclusion_of :done_ratio, :in => 0..100
70 validates_inclusion_of :done_ratio, :in => 0..100
71 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
72 validates :start_date, :date => true
72 validates :start_date, :date => true
73 validates :due_date, :date => true
73 validates :due_date, :date => true
74 validate :validate_issue, :validate_required_fields
74 validate :validate_issue, :validate_required_fields
75
75
76 scope :visible, lambda {|*args|
76 scope :visible, lambda {|*args|
77 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
78 }
78 }
79
79
80 scope :open, lambda {|*args|
80 scope :open, lambda {|*args|
81 is_closed = args.size > 0 ? !args.first : false
81 is_closed = args.size > 0 ? !args.first : false
82 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
83 }
83 }
84
84
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
86 scope :on_active_project, lambda {
86 scope :on_active_project, lambda {
87 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
88 }
88 }
89 scope :fixed_version, lambda {|versions|
89 scope :fixed_version, lambda {|versions|
90 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
91 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
92 }
92 }
93
93
94 before_create :default_assign
94 before_create :default_assign
95 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
95 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
96 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
96 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
97 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
97 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
98 # Should be after_create but would be called before previous after_save callbacks
98 # Should be after_create but would be called before previous after_save callbacks
99 after_save :after_create_from_copy
99 after_save :after_create_from_copy
100 after_destroy :update_parent_attributes
100 after_destroy :update_parent_attributes
101 after_create :send_notification
101 after_create :send_notification
102
102
103 # Returns a SQL conditions string used to find all issues visible by the specified user
103 # Returns a SQL conditions string used to find all issues visible by the specified user
104 def self.visible_condition(user, options={})
104 def self.visible_condition(user, options={})
105 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
105 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
106 if user.logged?
106 if user.logged?
107 case role.issues_visibility
107 case role.issues_visibility
108 when 'all'
108 when 'all'
109 nil
109 nil
110 when 'default'
110 when 'default'
111 user_ids = [user.id] + user.groups.map(&:id)
111 user_ids = [user.id] + user.groups.map(&:id)
112 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
112 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
113 when 'own'
113 when 'own'
114 user_ids = [user.id] + user.groups.map(&:id)
114 user_ids = [user.id] + user.groups.map(&:id)
115 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
115 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
116 else
116 else
117 '1=0'
117 '1=0'
118 end
118 end
119 else
119 else
120 "(#{table_name}.is_private = #{connection.quoted_false})"
120 "(#{table_name}.is_private = #{connection.quoted_false})"
121 end
121 end
122 end
122 end
123 end
123 end
124
124
125 # Returns true if usr or current user is allowed to view the issue
125 # Returns true if usr or current user is allowed to view the issue
126 def visible?(usr=nil)
126 def visible?(usr=nil)
127 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
127 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
128 if user.logged?
128 if user.logged?
129 case role.issues_visibility
129 case role.issues_visibility
130 when 'all'
130 when 'all'
131 true
131 true
132 when 'default'
132 when 'default'
133 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
133 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
134 when 'own'
134 when 'own'
135 self.author == user || user.is_or_belongs_to?(assigned_to)
135 self.author == user || user.is_or_belongs_to?(assigned_to)
136 else
136 else
137 false
137 false
138 end
138 end
139 else
139 else
140 !self.is_private?
140 !self.is_private?
141 end
141 end
142 end
142 end
143 end
143 end
144
144
145 # Returns true if user or current user is allowed to edit or add a note to the issue
145 # Returns true if user or current user is allowed to edit or add a note to the issue
146 def editable?(user=User.current)
146 def editable?(user=User.current)
147 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
147 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
148 end
148 end
149
149
150 def initialize(attributes=nil, *args)
150 def initialize(attributes=nil, *args)
151 super
151 super
152 if new_record?
152 if new_record?
153 # set default values for new records only
153 # set default values for new records only
154 self.status ||= IssueStatus.default
154 self.status ||= IssueStatus.default
155 self.priority ||= IssuePriority.default
155 self.priority ||= IssuePriority.default
156 self.watcher_user_ids = []
156 self.watcher_user_ids = []
157 end
157 end
158 end
158 end
159
159
160 def create_or_update
160 def create_or_update
161 super
161 super
162 ensure
162 ensure
163 @status_was = nil
163 @status_was = nil
164 end
164 end
165 private :create_or_update
165 private :create_or_update
166
166
167 # AR#Persistence#destroy would raise and RecordNotFound exception
167 # AR#Persistence#destroy would raise and RecordNotFound exception
168 # if the issue was already deleted or updated (non matching lock_version).
168 # if the issue was already deleted or updated (non matching lock_version).
169 # This is a problem when bulk deleting issues or deleting a project
169 # This is a problem when bulk deleting issues or deleting a project
170 # (because an issue may already be deleted if its parent was deleted
170 # (because an issue may already be deleted if its parent was deleted
171 # first).
171 # first).
172 # The issue is reloaded by the nested_set before being deleted so
172 # The issue is reloaded by the nested_set before being deleted so
173 # the lock_version condition should not be an issue but we handle it.
173 # the lock_version condition should not be an issue but we handle it.
174 def destroy
174 def destroy
175 super
175 super
176 rescue ActiveRecord::RecordNotFound
176 rescue ActiveRecord::RecordNotFound
177 # Stale or already deleted
177 # Stale or already deleted
178 begin
178 begin
179 reload
179 reload
180 rescue ActiveRecord::RecordNotFound
180 rescue ActiveRecord::RecordNotFound
181 # The issue was actually already deleted
181 # The issue was actually already deleted
182 @destroyed = true
182 @destroyed = true
183 return freeze
183 return freeze
184 end
184 end
185 # The issue was stale, retry to destroy
185 # The issue was stale, retry to destroy
186 super
186 super
187 end
187 end
188
188
189 alias :base_reload :reload
189 alias :base_reload :reload
190 def reload(*args)
190 def reload(*args)
191 @workflow_rule_by_attribute = nil
191 @workflow_rule_by_attribute = nil
192 @assignable_versions = nil
192 @assignable_versions = nil
193 @relations = nil
193 @relations = nil
194 base_reload(*args)
194 base_reload(*args)
195 end
195 end
196
196
197 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
197 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
198 def available_custom_fields
198 def available_custom_fields
199 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
199 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
200 end
200 end
201
201
202 def visible_custom_field_values(user=nil)
202 def visible_custom_field_values(user=nil)
203 user_real = user || User.current
203 user_real = user || User.current
204 custom_field_values.select do |value|
204 custom_field_values.select do |value|
205 value.custom_field.visible_by?(project, user_real)
205 value.custom_field.visible_by?(project, user_real)
206 end
206 end
207 end
207 end
208
208
209 # Copies attributes from another issue, arg can be an id or an Issue
209 # Copies attributes from another issue, arg can be an id or an Issue
210 def copy_from(arg, options={})
210 def copy_from(arg, options={})
211 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
211 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
212 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
212 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
213 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
213 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
214 self.status = issue.status
214 self.status = issue.status
215 self.author = User.current
215 self.author = User.current
216 unless options[:attachments] == false
216 unless options[:attachments] == false
217 self.attachments = issue.attachments.map do |attachement|
217 self.attachments = issue.attachments.map do |attachement|
218 attachement.copy(:container => self)
218 attachement.copy(:container => self)
219 end
219 end
220 end
220 end
221 @copied_from = issue
221 @copied_from = issue
222 @copy_options = options
222 @copy_options = options
223 self
223 self
224 end
224 end
225
225
226 # Returns an unsaved copy of the issue
226 # Returns an unsaved copy of the issue
227 def copy(attributes=nil, copy_options={})
227 def copy(attributes=nil, copy_options={})
228 copy = self.class.new.copy_from(self, copy_options)
228 copy = self.class.new.copy_from(self, copy_options)
229 copy.attributes = attributes if attributes
229 copy.attributes = attributes if attributes
230 copy
230 copy
231 end
231 end
232
232
233 # Returns true if the issue is a copy
233 # Returns true if the issue is a copy
234 def copy?
234 def copy?
235 @copied_from.present?
235 @copied_from.present?
236 end
236 end
237
237
238 # Moves/copies an issue to a new project and tracker
238 # Moves/copies an issue to a new project and tracker
239 # Returns the moved/copied issue on success, false on failure
239 # Returns the moved/copied issue on success, false on failure
240 def move_to_project(new_project, new_tracker=nil, options={})
240 def move_to_project(new_project, new_tracker=nil, options={})
241 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
241 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
242
242
243 if options[:copy]
243 if options[:copy]
244 issue = self.copy
244 issue = self.copy
245 else
245 else
246 issue = self
246 issue = self
247 end
247 end
248
248
249 issue.init_journal(User.current, options[:notes])
249 issue.init_journal(User.current, options[:notes])
250
250
251 # Preserve previous behaviour
251 # Preserve previous behaviour
252 # #move_to_project doesn't change tracker automatically
252 # #move_to_project doesn't change tracker automatically
253 issue.send :project=, new_project, true
253 issue.send :project=, new_project, true
254 if new_tracker
254 if new_tracker
255 issue.tracker = new_tracker
255 issue.tracker = new_tracker
256 end
256 end
257 # Allow bulk setting of attributes on the issue
257 # Allow bulk setting of attributes on the issue
258 if options[:attributes]
258 if options[:attributes]
259 issue.attributes = options[:attributes]
259 issue.attributes = options[:attributes]
260 end
260 end
261
261
262 issue.save ? issue : false
262 issue.save ? issue : false
263 end
263 end
264
264
265 def status_id=(sid)
265 def status_id=(sid)
266 self.status = nil
266 self.status = nil
267 result = write_attribute(:status_id, sid)
267 result = write_attribute(:status_id, sid)
268 @workflow_rule_by_attribute = nil
268 @workflow_rule_by_attribute = nil
269 result
269 result
270 end
270 end
271
271
272 def priority_id=(pid)
272 def priority_id=(pid)
273 self.priority = nil
273 self.priority = nil
274 write_attribute(:priority_id, pid)
274 write_attribute(:priority_id, pid)
275 end
275 end
276
276
277 def category_id=(cid)
277 def category_id=(cid)
278 self.category = nil
278 self.category = nil
279 write_attribute(:category_id, cid)
279 write_attribute(:category_id, cid)
280 end
280 end
281
281
282 def fixed_version_id=(vid)
282 def fixed_version_id=(vid)
283 self.fixed_version = nil
283 self.fixed_version = nil
284 write_attribute(:fixed_version_id, vid)
284 write_attribute(:fixed_version_id, vid)
285 end
285 end
286
286
287 def tracker_id=(tid)
287 def tracker_id=(tid)
288 self.tracker = nil
288 self.tracker = nil
289 result = write_attribute(:tracker_id, tid)
289 result = write_attribute(:tracker_id, tid)
290 @custom_field_values = nil
290 @custom_field_values = nil
291 @workflow_rule_by_attribute = nil
291 @workflow_rule_by_attribute = nil
292 result
292 result
293 end
293 end
294
294
295 def project_id=(project_id)
295 def project_id=(project_id)
296 if project_id.to_s != self.project_id.to_s
296 if project_id.to_s != self.project_id.to_s
297 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
297 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
298 end
298 end
299 end
299 end
300
300
301 def project=(project, keep_tracker=false)
301 def project=(project, keep_tracker=false)
302 project_was = self.project
302 project_was = self.project
303 write_attribute(:project_id, project ? project.id : nil)
303 write_attribute(:project_id, project ? project.id : nil)
304 association_instance_set('project', project)
304 association_instance_set('project', project)
305 if project_was && project && project_was != project
305 if project_was && project && project_was != project
306 @assignable_versions = nil
306 @assignable_versions = nil
307
307
308 unless keep_tracker || project.trackers.include?(tracker)
308 unless keep_tracker || project.trackers.include?(tracker)
309 self.tracker = project.trackers.first
309 self.tracker = project.trackers.first
310 end
310 end
311 # Reassign to the category with same name if any
311 # Reassign to the category with same name if any
312 if category
312 if category
313 self.category = project.issue_categories.find_by_name(category.name)
313 self.category = project.issue_categories.find_by_name(category.name)
314 end
314 end
315 # Keep the fixed_version if it's still valid in the new_project
315 # Keep the fixed_version if it's still valid in the new_project
316 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
316 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
317 self.fixed_version = nil
317 self.fixed_version = nil
318 end
318 end
319 # Clear the parent task if it's no longer valid
319 # Clear the parent task if it's no longer valid
320 unless valid_parent_project?
320 unless valid_parent_project?
321 self.parent_issue_id = nil
321 self.parent_issue_id = nil
322 end
322 end
323 @custom_field_values = nil
323 @custom_field_values = nil
324 end
324 end
325 end
325 end
326
326
327 def description=(arg)
327 def description=(arg)
328 if arg.is_a?(String)
328 if arg.is_a?(String)
329 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
329 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
330 end
330 end
331 write_attribute(:description, arg)
331 write_attribute(:description, arg)
332 end
332 end
333
333
334 # Overrides assign_attributes so that project and tracker get assigned first
334 # Overrides assign_attributes so that project and tracker get assigned first
335 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
335 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
336 return if new_attributes.nil?
336 return if new_attributes.nil?
337 attrs = new_attributes.dup
337 attrs = new_attributes.dup
338 attrs.stringify_keys!
338 attrs.stringify_keys!
339
339
340 %w(project project_id tracker tracker_id).each do |attr|
340 %w(project project_id tracker tracker_id).each do |attr|
341 if attrs.has_key?(attr)
341 if attrs.has_key?(attr)
342 send "#{attr}=", attrs.delete(attr)
342 send "#{attr}=", attrs.delete(attr)
343 end
343 end
344 end
344 end
345 send :assign_attributes_without_project_and_tracker_first, attrs, *args
345 send :assign_attributes_without_project_and_tracker_first, attrs, *args
346 end
346 end
347 # Do not redefine alias chain on reload (see #4838)
347 # Do not redefine alias chain on reload (see #4838)
348 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
348 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
349
349
350 def estimated_hours=(h)
350 def estimated_hours=(h)
351 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
351 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
352 end
352 end
353
353
354 safe_attributes 'project_id',
354 safe_attributes 'project_id',
355 :if => lambda {|issue, user|
355 :if => lambda {|issue, user|
356 if issue.new_record?
356 if issue.new_record?
357 issue.copy?
357 issue.copy?
358 elsif user.allowed_to?(:move_issues, issue.project)
358 elsif user.allowed_to?(:move_issues, issue.project)
359 Issue.allowed_target_projects_on_move.count > 1
359 Issue.allowed_target_projects_on_move.count > 1
360 end
360 end
361 }
361 }
362
362
363 safe_attributes 'tracker_id',
363 safe_attributes 'tracker_id',
364 'status_id',
364 'status_id',
365 'category_id',
365 'category_id',
366 'assigned_to_id',
366 'assigned_to_id',
367 'priority_id',
367 'priority_id',
368 'fixed_version_id',
368 'fixed_version_id',
369 'subject',
369 'subject',
370 'description',
370 'description',
371 'start_date',
371 'start_date',
372 'due_date',
372 'due_date',
373 'done_ratio',
373 'done_ratio',
374 'estimated_hours',
374 'estimated_hours',
375 'custom_field_values',
375 'custom_field_values',
376 'custom_fields',
376 'custom_fields',
377 'lock_version',
377 'lock_version',
378 'notes',
378 'notes',
379 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
379 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
380
380
381 safe_attributes 'status_id',
381 safe_attributes 'status_id',
382 'assigned_to_id',
382 'assigned_to_id',
383 'fixed_version_id',
383 'fixed_version_id',
384 'done_ratio',
384 'done_ratio',
385 'lock_version',
385 'lock_version',
386 'notes',
386 'notes',
387 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
387 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
388
388
389 safe_attributes 'notes',
389 safe_attributes 'notes',
390 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
390 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
391
391
392 safe_attributes 'private_notes',
392 safe_attributes 'private_notes',
393 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
393 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
394
394
395 safe_attributes 'watcher_user_ids',
395 safe_attributes 'watcher_user_ids',
396 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
396 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
397
397
398 safe_attributes 'is_private',
398 safe_attributes 'is_private',
399 :if => lambda {|issue, user|
399 :if => lambda {|issue, user|
400 user.allowed_to?(:set_issues_private, issue.project) ||
400 user.allowed_to?(:set_issues_private, issue.project) ||
401 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
401 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
402 }
402 }
403
403
404 safe_attributes 'parent_issue_id',
404 safe_attributes 'parent_issue_id',
405 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
405 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
406 user.allowed_to?(:manage_subtasks, issue.project)}
406 user.allowed_to?(:manage_subtasks, issue.project)}
407
407
408 def safe_attribute_names(user=nil)
408 def safe_attribute_names(user=nil)
409 names = super
409 names = super
410 names -= disabled_core_fields
410 names -= disabled_core_fields
411 names -= read_only_attribute_names(user)
411 names -= read_only_attribute_names(user)
412 names
412 names
413 end
413 end
414
414
415 # Safely sets attributes
415 # Safely sets attributes
416 # Should be called from controllers instead of #attributes=
416 # Should be called from controllers instead of #attributes=
417 # attr_accessible is too rough because we still want things like
417 # attr_accessible is too rough because we still want things like
418 # Issue.new(:project => foo) to work
418 # Issue.new(:project => foo) to work
419 def safe_attributes=(attrs, user=User.current)
419 def safe_attributes=(attrs, user=User.current)
420 return unless attrs.is_a?(Hash)
420 return unless attrs.is_a?(Hash)
421
421
422 attrs = attrs.dup
422 attrs = attrs.dup
423
423
424 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
424 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
425 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
425 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
426 if allowed_target_projects(user).where(:id => p.to_i).exists?
426 if allowed_target_projects(user).where(:id => p.to_i).exists?
427 self.project_id = p
427 self.project_id = p
428 end
428 end
429 end
429 end
430
430
431 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
431 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
432 self.tracker_id = t
432 self.tracker_id = t
433 end
433 end
434
434
435 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
435 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
436 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
436 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
437 self.status_id = s
437 self.status_id = s
438 end
438 end
439 end
439 end
440
440
441 attrs = delete_unsafe_attributes(attrs, user)
441 attrs = delete_unsafe_attributes(attrs, user)
442 return if attrs.empty?
442 return if attrs.empty?
443
443
444 unless leaf?
444 unless leaf?
445 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
445 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
446 end
446 end
447
447
448 if attrs['parent_issue_id'].present?
448 if attrs['parent_issue_id'].present?
449 s = attrs['parent_issue_id'].to_s
449 s = attrs['parent_issue_id'].to_s
450 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
450 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
451 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
451 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
452 end
452 end
453 end
453 end
454
454
455 if attrs['custom_field_values'].present?
455 if attrs['custom_field_values'].present?
456 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
456 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
457 # TODO: use #select when ruby1.8 support is dropped
457 # TODO: use #select when ruby1.8 support is dropped
458 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| !editable_custom_field_ids.include?(k.to_s)}
458 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| !editable_custom_field_ids.include?(k.to_s)}
459 end
459 end
460
460
461 if attrs['custom_fields'].present?
461 if attrs['custom_fields'].present?
462 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
462 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
463 # TODO: use #select when ruby1.8 support is dropped
463 # TODO: use #select when ruby1.8 support is dropped
464 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| !editable_custom_field_ids.include?(c['id'].to_s)}
464 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| !editable_custom_field_ids.include?(c['id'].to_s)}
465 end
465 end
466
466
467 # mass-assignment security bypass
467 # mass-assignment security bypass
468 assign_attributes attrs, :without_protection => true
468 assign_attributes attrs, :without_protection => true
469 end
469 end
470
470
471 def disabled_core_fields
471 def disabled_core_fields
472 tracker ? tracker.disabled_core_fields : []
472 tracker ? tracker.disabled_core_fields : []
473 end
473 end
474
474
475 # Returns the custom_field_values that can be edited by the given user
475 # Returns the custom_field_values that can be edited by the given user
476 def editable_custom_field_values(user=nil)
476 def editable_custom_field_values(user=nil)
477 visible_custom_field_values(user).reject do |value|
477 visible_custom_field_values(user).reject do |value|
478 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
478 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
479 end
479 end
480 end
480 end
481
481
482 # Returns the names of attributes that are read-only for user or the current user
482 # Returns the names of attributes that are read-only for user or the current user
483 # For users with multiple roles, the read-only fields are the intersection of
483 # For users with multiple roles, the read-only fields are the intersection of
484 # read-only fields of each role
484 # read-only fields of each role
485 # The result is an array of strings where sustom fields are represented with their ids
485 # The result is an array of strings where sustom fields are represented with their ids
486 #
486 #
487 # Examples:
487 # Examples:
488 # issue.read_only_attribute_names # => ['due_date', '2']
488 # issue.read_only_attribute_names # => ['due_date', '2']
489 # issue.read_only_attribute_names(user) # => []
489 # issue.read_only_attribute_names(user) # => []
490 def read_only_attribute_names(user=nil)
490 def read_only_attribute_names(user=nil)
491 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
491 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
492 end
492 end
493
493
494 # Returns the names of required attributes for user or the current user
494 # Returns the names of required attributes for user or the current user
495 # For users with multiple roles, the required fields are the intersection of
495 # For users with multiple roles, the required fields are the intersection of
496 # required fields of each role
496 # required fields of each role
497 # The result is an array of strings where sustom fields are represented with their ids
497 # The result is an array of strings where sustom fields are represented with their ids
498 #
498 #
499 # Examples:
499 # Examples:
500 # issue.required_attribute_names # => ['due_date', '2']
500 # issue.required_attribute_names # => ['due_date', '2']
501 # issue.required_attribute_names(user) # => []
501 # issue.required_attribute_names(user) # => []
502 def required_attribute_names(user=nil)
502 def required_attribute_names(user=nil)
503 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
503 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
504 end
504 end
505
505
506 # Returns true if the attribute is required for user
506 # Returns true if the attribute is required for user
507 def required_attribute?(name, user=nil)
507 def required_attribute?(name, user=nil)
508 required_attribute_names(user).include?(name.to_s)
508 required_attribute_names(user).include?(name.to_s)
509 end
509 end
510
510
511 # Returns a hash of the workflow rule by attribute for the given user
511 # Returns a hash of the workflow rule by attribute for the given user
512 #
512 #
513 # Examples:
513 # Examples:
514 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
514 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
515 def workflow_rule_by_attribute(user=nil)
515 def workflow_rule_by_attribute(user=nil)
516 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
516 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
517
517
518 user_real = user || User.current
518 user_real = user || User.current
519 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
519 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
520 return {} if roles.empty?
520 return {} if roles.empty?
521
521
522 result = {}
522 result = {}
523 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
523 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
524 if workflow_permissions.any?
524 if workflow_permissions.any?
525 workflow_rules = workflow_permissions.inject({}) do |h, wp|
525 workflow_rules = workflow_permissions.inject({}) do |h, wp|
526 h[wp.field_name] ||= []
526 h[wp.field_name] ||= []
527 h[wp.field_name] << wp.rule
527 h[wp.field_name] << wp.rule
528 h
528 h
529 end
529 end
530 workflow_rules.each do |attr, rules|
530 workflow_rules.each do |attr, rules|
531 next if rules.size < roles.size
531 next if rules.size < roles.size
532 uniq_rules = rules.uniq
532 uniq_rules = rules.uniq
533 if uniq_rules.size == 1
533 if uniq_rules.size == 1
534 result[attr] = uniq_rules.first
534 result[attr] = uniq_rules.first
535 else
535 else
536 result[attr] = 'required'
536 result[attr] = 'required'
537 end
537 end
538 end
538 end
539 end
539 end
540 @workflow_rule_by_attribute = result if user.nil?
540 @workflow_rule_by_attribute = result if user.nil?
541 result
541 result
542 end
542 end
543 private :workflow_rule_by_attribute
543 private :workflow_rule_by_attribute
544
544
545 def done_ratio
545 def done_ratio
546 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
546 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
547 status.default_done_ratio
547 status.default_done_ratio
548 else
548 else
549 read_attribute(:done_ratio)
549 read_attribute(:done_ratio)
550 end
550 end
551 end
551 end
552
552
553 def self.use_status_for_done_ratio?
553 def self.use_status_for_done_ratio?
554 Setting.issue_done_ratio == 'issue_status'
554 Setting.issue_done_ratio == 'issue_status'
555 end
555 end
556
556
557 def self.use_field_for_done_ratio?
557 def self.use_field_for_done_ratio?
558 Setting.issue_done_ratio == 'issue_field'
558 Setting.issue_done_ratio == 'issue_field'
559 end
559 end
560
560
561 def validate_issue
561 def validate_issue
562 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
562 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
563 errors.add :due_date, :greater_than_start_date
563 errors.add :due_date, :greater_than_start_date
564 end
564 end
565
565
566 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
566 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
567 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
567 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
568 end
568 end
569
569
570 if fixed_version
570 if fixed_version
571 if !assignable_versions.include?(fixed_version)
571 if !assignable_versions.include?(fixed_version)
572 errors.add :fixed_version_id, :inclusion
572 errors.add :fixed_version_id, :inclusion
573 elsif reopened? && fixed_version.closed?
573 elsif reopened? && fixed_version.closed?
574 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
574 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
575 end
575 end
576 end
576 end
577
577
578 # Checks that the issue can not be added/moved to a disabled tracker
578 # Checks that the issue can not be added/moved to a disabled tracker
579 if project && (tracker_id_changed? || project_id_changed?)
579 if project && (tracker_id_changed? || project_id_changed?)
580 unless project.trackers.include?(tracker)
580 unless project.trackers.include?(tracker)
581 errors.add :tracker_id, :inclusion
581 errors.add :tracker_id, :inclusion
582 end
582 end
583 end
583 end
584
584
585 # Checks parent issue assignment
585 # Checks parent issue assignment
586 if @invalid_parent_issue_id.present?
586 if @invalid_parent_issue_id.present?
587 errors.add :parent_issue_id, :invalid
587 errors.add :parent_issue_id, :invalid
588 elsif @parent_issue
588 elsif @parent_issue
589 if !valid_parent_project?(@parent_issue)
589 if !valid_parent_project?(@parent_issue)
590 errors.add :parent_issue_id, :invalid
590 errors.add :parent_issue_id, :invalid
591 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
591 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
592 errors.add :parent_issue_id, :invalid
592 errors.add :parent_issue_id, :invalid
593 elsif !new_record?
593 elsif !new_record?
594 # moving an existing issue
594 # moving an existing issue
595 if @parent_issue.root_id != root_id
595 if @parent_issue.root_id != root_id
596 # we can always move to another tree
596 # we can always move to another tree
597 elsif move_possible?(@parent_issue)
597 elsif move_possible?(@parent_issue)
598 # move accepted inside tree
598 # move accepted inside tree
599 else
599 else
600 errors.add :parent_issue_id, :invalid
600 errors.add :parent_issue_id, :invalid
601 end
601 end
602 end
602 end
603 end
603 end
604 end
604 end
605
605
606 # Validates the issue against additional workflow requirements
606 # Validates the issue against additional workflow requirements
607 def validate_required_fields
607 def validate_required_fields
608 user = new_record? ? author : current_journal.try(:user)
608 user = new_record? ? author : current_journal.try(:user)
609
609
610 required_attribute_names(user).each do |attribute|
610 required_attribute_names(user).each do |attribute|
611 if attribute =~ /^\d+$/
611 if attribute =~ /^\d+$/
612 attribute = attribute.to_i
612 attribute = attribute.to_i
613 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
613 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
614 if v && v.value.blank?
614 if v && v.value.blank?
615 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
615 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
616 end
616 end
617 else
617 else
618 if respond_to?(attribute) && send(attribute).blank?
618 if respond_to?(attribute) && send(attribute).blank?
619 errors.add attribute, :blank
619 errors.add attribute, :blank
620 end
620 end
621 end
621 end
622 end
622 end
623 end
623 end
624
624
625 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
625 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
626 # even if the user turns off the setting later
626 # even if the user turns off the setting later
627 def update_done_ratio_from_issue_status
627 def update_done_ratio_from_issue_status
628 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
628 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
629 self.done_ratio = status.default_done_ratio
629 self.done_ratio = status.default_done_ratio
630 end
630 end
631 end
631 end
632
632
633 def init_journal(user, notes = "")
633 def init_journal(user, notes = "")
634 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
634 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
635 if new_record?
635 if new_record?
636 @current_journal.notify = false
636 @current_journal.notify = false
637 else
637 else
638 @attributes_before_change = attributes.dup
638 @attributes_before_change = attributes.dup
639 @custom_values_before_change = {}
639 @custom_values_before_change = {}
640 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
640 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
641 end
641 end
642 @current_journal
642 @current_journal
643 end
643 end
644
644
645 # Returns the id of the last journal or nil
645 # Returns the id of the last journal or nil
646 def last_journal_id
646 def last_journal_id
647 if new_record?
647 if new_record?
648 nil
648 nil
649 else
649 else
650 journals.maximum(:id)
650 journals.maximum(:id)
651 end
651 end
652 end
652 end
653
653
654 # Returns a scope for journals that have an id greater than journal_id
654 # Returns a scope for journals that have an id greater than journal_id
655 def journals_after(journal_id)
655 def journals_after(journal_id)
656 scope = journals.reorder("#{Journal.table_name}.id ASC")
656 scope = journals.reorder("#{Journal.table_name}.id ASC")
657 if journal_id.present?
657 if journal_id.present?
658 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
658 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
659 end
659 end
660 scope
660 scope
661 end
661 end
662
662
663 # Returns the initial status of the issue
663 # Returns the initial status of the issue
664 # Returns nil for a new issue
664 # Returns nil for a new issue
665 def status_was
665 def status_was
666 if status_id_was && status_id_was.to_i > 0
666 if status_id_was && status_id_was.to_i > 0
667 @status_was ||= IssueStatus.find_by_id(status_id_was)
667 @status_was ||= IssueStatus.find_by_id(status_id_was)
668 end
668 end
669 end
669 end
670
670
671 # Return true if the issue is closed, otherwise false
671 # Return true if the issue is closed, otherwise false
672 def closed?
672 def closed?
673 self.status.is_closed?
673 self.status.is_closed?
674 end
674 end
675
675
676 # Return true if the issue is being reopened
676 # Return true if the issue is being reopened
677 def reopened?
677 def reopened?
678 if !new_record? && status_id_changed?
678 if !new_record? && status_id_changed?
679 status_was = IssueStatus.find_by_id(status_id_was)
679 status_was = IssueStatus.find_by_id(status_id_was)
680 status_new = IssueStatus.find_by_id(status_id)
680 status_new = IssueStatus.find_by_id(status_id)
681 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
681 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
682 return true
682 return true
683 end
683 end
684 end
684 end
685 false
685 false
686 end
686 end
687
687
688 # Return true if the issue is being closed
688 # Return true if the issue is being closed
689 def closing?
689 def closing?
690 if !new_record? && status_id_changed?
690 if !new_record? && status_id_changed?
691 if status_was && status && !status_was.is_closed? && status.is_closed?
691 if status_was && status && !status_was.is_closed? && status.is_closed?
692 return true
692 return true
693 end
693 end
694 end
694 end
695 false
695 false
696 end
696 end
697
697
698 # Returns true if the issue is overdue
698 # Returns true if the issue is overdue
699 def overdue?
699 def overdue?
700 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
700 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
701 end
701 end
702
702
703 # Is the amount of work done less than it should for the due date
703 # Is the amount of work done less than it should for the due date
704 def behind_schedule?
704 def behind_schedule?
705 return false if start_date.nil? || due_date.nil?
705 return false if start_date.nil? || due_date.nil?
706 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
706 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
707 return done_date <= Date.today
707 return done_date <= Date.today
708 end
708 end
709
709
710 # Does this issue have children?
710 # Does this issue have children?
711 def children?
711 def children?
712 !leaf?
712 !leaf?
713 end
713 end
714
714
715 # Users the issue can be assigned to
715 # Users the issue can be assigned to
716 def assignable_users
716 def assignable_users
717 users = project.assignable_users
717 users = project.assignable_users
718 users << author if author
718 users << author if author
719 users << assigned_to if assigned_to
719 users << assigned_to if assigned_to
720 users.uniq.sort
720 users.uniq.sort
721 end
721 end
722
722
723 # Versions that the issue can be assigned to
723 # Versions that the issue can be assigned to
724 def assignable_versions
724 def assignable_versions
725 return @assignable_versions if @assignable_versions
725 return @assignable_versions if @assignable_versions
726
726
727 versions = project.shared_versions.open.all
727 versions = project.shared_versions.open.all
728 if fixed_version
728 if fixed_version
729 if fixed_version_id_changed?
729 if fixed_version_id_changed?
730 # nothing to do
730 # nothing to do
731 elsif project_id_changed?
731 elsif project_id_changed?
732 if project.shared_versions.include?(fixed_version)
732 if project.shared_versions.include?(fixed_version)
733 versions << fixed_version
733 versions << fixed_version
734 end
734 end
735 else
735 else
736 versions << fixed_version
736 versions << fixed_version
737 end
737 end
738 end
738 end
739 @assignable_versions = versions.uniq.sort
739 @assignable_versions = versions.uniq.sort
740 end
740 end
741
741
742 # Returns true if this issue is blocked by another issue that is still open
742 # Returns true if this issue is blocked by another issue that is still open
743 def blocked?
743 def blocked?
744 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
744 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
745 end
745 end
746
746
747 # Returns an array of statuses that user is able to apply
747 # Returns an array of statuses that user is able to apply
748 def new_statuses_allowed_to(user=User.current, include_default=false)
748 def new_statuses_allowed_to(user=User.current, include_default=false)
749 if new_record? && @copied_from
749 if new_record? && @copied_from
750 [IssueStatus.default, @copied_from.status].compact.uniq.sort
750 [IssueStatus.default, @copied_from.status].compact.uniq.sort
751 else
751 else
752 initial_status = nil
752 initial_status = nil
753 if new_record?
753 if new_record?
754 initial_status = IssueStatus.default
754 initial_status = IssueStatus.default
755 elsif status_id_was
755 elsif status_id_was
756 initial_status = IssueStatus.find_by_id(status_id_was)
756 initial_status = IssueStatus.find_by_id(status_id_was)
757 end
757 end
758 initial_status ||= status
758 initial_status ||= status
759
759
760 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
761 assignee_transitions_allowed = initial_assigned_to_id.present? &&
762 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
763
760 statuses = initial_status.find_new_statuses_allowed_to(
764 statuses = initial_status.find_new_statuses_allowed_to(
761 user.admin ? Role.all : user.roles_for_project(project),
765 user.admin ? Role.all : user.roles_for_project(project),
762 tracker,
766 tracker,
763 author == user,
767 author == user,
764 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
768 assignee_transitions_allowed
765 )
769 )
766 statuses << initial_status unless statuses.empty?
770 statuses << initial_status unless statuses.empty?
767 statuses << IssueStatus.default if include_default
771 statuses << IssueStatus.default if include_default
768 statuses = statuses.compact.uniq.sort
772 statuses = statuses.compact.uniq.sort
769 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
773 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
770 end
774 end
771 end
775 end
772
776
773 def assigned_to_was
777 def assigned_to_was
774 if assigned_to_id_changed? && assigned_to_id_was.present?
778 if assigned_to_id_changed? && assigned_to_id_was.present?
775 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
779 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
776 end
780 end
777 end
781 end
778
782
779 # Returns the users that should be notified
783 # Returns the users that should be notified
780 def notified_users
784 def notified_users
781 notified = []
785 notified = []
782 # Author and assignee are always notified unless they have been
786 # Author and assignee are always notified unless they have been
783 # locked or don't want to be notified
787 # locked or don't want to be notified
784 notified << author if author
788 notified << author if author
785 if assigned_to
789 if assigned_to
786 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
790 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
787 end
791 end
788 if assigned_to_was
792 if assigned_to_was
789 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
793 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
790 end
794 end
791 notified = notified.select {|u| u.active? && u.notify_about?(self)}
795 notified = notified.select {|u| u.active? && u.notify_about?(self)}
792
796
793 notified += project.notified_users
797 notified += project.notified_users
794 notified.uniq!
798 notified.uniq!
795 # Remove users that can not view the issue
799 # Remove users that can not view the issue
796 notified.reject! {|user| !visible?(user)}
800 notified.reject! {|user| !visible?(user)}
797 notified
801 notified
798 end
802 end
799
803
800 # Returns the email addresses that should be notified
804 # Returns the email addresses that should be notified
801 def recipients
805 def recipients
802 notified_users.collect(&:mail)
806 notified_users.collect(&:mail)
803 end
807 end
804
808
805 def each_notification(users, &block)
809 def each_notification(users, &block)
806 if users.any?
810 if users.any?
807 if custom_field_values.detect {|value| !value.custom_field.visible?}
811 if custom_field_values.detect {|value| !value.custom_field.visible?}
808 users_by_custom_field_visibility = users.group_by do |user|
812 users_by_custom_field_visibility = users.group_by do |user|
809 visible_custom_field_values(user).map(&:custom_field_id).sort
813 visible_custom_field_values(user).map(&:custom_field_id).sort
810 end
814 end
811 users_by_custom_field_visibility.values.each do |users|
815 users_by_custom_field_visibility.values.each do |users|
812 yield(users)
816 yield(users)
813 end
817 end
814 else
818 else
815 yield(users)
819 yield(users)
816 end
820 end
817 end
821 end
818 end
822 end
819
823
820 # Returns the number of hours spent on this issue
824 # Returns the number of hours spent on this issue
821 def spent_hours
825 def spent_hours
822 @spent_hours ||= time_entries.sum(:hours) || 0
826 @spent_hours ||= time_entries.sum(:hours) || 0
823 end
827 end
824
828
825 # Returns the total number of hours spent on this issue and its descendants
829 # Returns the total number of hours spent on this issue and its descendants
826 #
830 #
827 # Example:
831 # Example:
828 # spent_hours => 0.0
832 # spent_hours => 0.0
829 # spent_hours => 50.2
833 # spent_hours => 50.2
830 def total_spent_hours
834 def total_spent_hours
831 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
835 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
832 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
836 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
833 end
837 end
834
838
835 def relations
839 def relations
836 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
840 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
837 end
841 end
838
842
839 # Preloads relations for a collection of issues
843 # Preloads relations for a collection of issues
840 def self.load_relations(issues)
844 def self.load_relations(issues)
841 if issues.any?
845 if issues.any?
842 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
846 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
843 issues.each do |issue|
847 issues.each do |issue|
844 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
848 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
845 end
849 end
846 end
850 end
847 end
851 end
848
852
849 # Preloads visible spent time for a collection of issues
853 # Preloads visible spent time for a collection of issues
850 def self.load_visible_spent_hours(issues, user=User.current)
854 def self.load_visible_spent_hours(issues, user=User.current)
851 if issues.any?
855 if issues.any?
852 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
856 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
853 issues.each do |issue|
857 issues.each do |issue|
854 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
858 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
855 end
859 end
856 end
860 end
857 end
861 end
858
862
859 # Preloads visible relations for a collection of issues
863 # Preloads visible relations for a collection of issues
860 def self.load_visible_relations(issues, user=User.current)
864 def self.load_visible_relations(issues, user=User.current)
861 if issues.any?
865 if issues.any?
862 issue_ids = issues.map(&:id)
866 issue_ids = issues.map(&:id)
863 # Relations with issue_from in given issues and visible issue_to
867 # Relations with issue_from in given issues and visible issue_to
864 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
868 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
865 # Relations with issue_to in given issues and visible issue_from
869 # Relations with issue_to in given issues and visible issue_from
866 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
870 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
867
871
868 issues.each do |issue|
872 issues.each do |issue|
869 relations =
873 relations =
870 relations_from.select {|relation| relation.issue_from_id == issue.id} +
874 relations_from.select {|relation| relation.issue_from_id == issue.id} +
871 relations_to.select {|relation| relation.issue_to_id == issue.id}
875 relations_to.select {|relation| relation.issue_to_id == issue.id}
872
876
873 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
877 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
874 end
878 end
875 end
879 end
876 end
880 end
877
881
878 # Finds an issue relation given its id.
882 # Finds an issue relation given its id.
879 def find_relation(relation_id)
883 def find_relation(relation_id)
880 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
884 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
881 end
885 end
882
886
883 # Returns all the other issues that depend on the issue
887 # Returns all the other issues that depend on the issue
884 # The algorithm is a modified breadth first search (bfs)
888 # The algorithm is a modified breadth first search (bfs)
885 def all_dependent_issues(except=[])
889 def all_dependent_issues(except=[])
886 # The found dependencies
890 # The found dependencies
887 dependencies = []
891 dependencies = []
888
892
889 # The visited flag for every node (issue) used by the breadth first search
893 # The visited flag for every node (issue) used by the breadth first search
890 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
894 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
891
895
892 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
896 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
893 # the issue when it is processed.
897 # the issue when it is processed.
894
898
895 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
899 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
896 # but its children will not be added to the queue when it is processed.
900 # but its children will not be added to the queue when it is processed.
897
901
898 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
902 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
899 # the queue, but its children have not been added.
903 # the queue, but its children have not been added.
900
904
901 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
905 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
902 # the children still need to be processed.
906 # the children still need to be processed.
903
907
904 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
908 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
905 # added as dependent issues. It needs no further processing.
909 # added as dependent issues. It needs no further processing.
906
910
907 issue_status = Hash.new(eNOT_DISCOVERED)
911 issue_status = Hash.new(eNOT_DISCOVERED)
908
912
909 # The queue
913 # The queue
910 queue = []
914 queue = []
911
915
912 # Initialize the bfs, add start node (self) to the queue
916 # Initialize the bfs, add start node (self) to the queue
913 queue << self
917 queue << self
914 issue_status[self] = ePROCESS_ALL
918 issue_status[self] = ePROCESS_ALL
915
919
916 while (!queue.empty?) do
920 while (!queue.empty?) do
917 current_issue = queue.shift
921 current_issue = queue.shift
918 current_issue_status = issue_status[current_issue]
922 current_issue_status = issue_status[current_issue]
919 dependencies << current_issue
923 dependencies << current_issue
920
924
921 # Add parent to queue, if not already in it.
925 # Add parent to queue, if not already in it.
922 parent = current_issue.parent
926 parent = current_issue.parent
923 parent_status = issue_status[parent]
927 parent_status = issue_status[parent]
924
928
925 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
929 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
926 queue << parent
930 queue << parent
927 issue_status[parent] = ePROCESS_RELATIONS_ONLY
931 issue_status[parent] = ePROCESS_RELATIONS_ONLY
928 end
932 end
929
933
930 # Add children to queue, but only if they are not already in it and
934 # Add children to queue, but only if they are not already in it and
931 # the children of the current node need to be processed.
935 # the children of the current node need to be processed.
932 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
936 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
933 current_issue.children.each do |child|
937 current_issue.children.each do |child|
934 next if except.include?(child)
938 next if except.include?(child)
935
939
936 if (issue_status[child] == eNOT_DISCOVERED)
940 if (issue_status[child] == eNOT_DISCOVERED)
937 queue << child
941 queue << child
938 issue_status[child] = ePROCESS_ALL
942 issue_status[child] = ePROCESS_ALL
939 elsif (issue_status[child] == eRELATIONS_PROCESSED)
943 elsif (issue_status[child] == eRELATIONS_PROCESSED)
940 queue << child
944 queue << child
941 issue_status[child] = ePROCESS_CHILDREN_ONLY
945 issue_status[child] = ePROCESS_CHILDREN_ONLY
942 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
946 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
943 queue << child
947 queue << child
944 issue_status[child] = ePROCESS_ALL
948 issue_status[child] = ePROCESS_ALL
945 end
949 end
946 end
950 end
947 end
951 end
948
952
949 # Add related issues to the queue, if they are not already in it.
953 # Add related issues to the queue, if they are not already in it.
950 current_issue.relations_from.map(&:issue_to).each do |related_issue|
954 current_issue.relations_from.map(&:issue_to).each do |related_issue|
951 next if except.include?(related_issue)
955 next if except.include?(related_issue)
952
956
953 if (issue_status[related_issue] == eNOT_DISCOVERED)
957 if (issue_status[related_issue] == eNOT_DISCOVERED)
954 queue << related_issue
958 queue << related_issue
955 issue_status[related_issue] = ePROCESS_ALL
959 issue_status[related_issue] = ePROCESS_ALL
956 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
960 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
957 queue << related_issue
961 queue << related_issue
958 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
962 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
959 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
963 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
960 queue << related_issue
964 queue << related_issue
961 issue_status[related_issue] = ePROCESS_ALL
965 issue_status[related_issue] = ePROCESS_ALL
962 end
966 end
963 end
967 end
964
968
965 # Set new status for current issue
969 # Set new status for current issue
966 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
970 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
967 issue_status[current_issue] = eALL_PROCESSED
971 issue_status[current_issue] = eALL_PROCESSED
968 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
972 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
969 issue_status[current_issue] = eRELATIONS_PROCESSED
973 issue_status[current_issue] = eRELATIONS_PROCESSED
970 end
974 end
971 end # while
975 end # while
972
976
973 # Remove the issues from the "except" parameter from the result array
977 # Remove the issues from the "except" parameter from the result array
974 dependencies -= except
978 dependencies -= except
975 dependencies.delete(self)
979 dependencies.delete(self)
976
980
977 dependencies
981 dependencies
978 end
982 end
979
983
980 # Returns an array of issues that duplicate this one
984 # Returns an array of issues that duplicate this one
981 def duplicates
985 def duplicates
982 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
986 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
983 end
987 end
984
988
985 # Returns the due date or the target due date if any
989 # Returns the due date or the target due date if any
986 # Used on gantt chart
990 # Used on gantt chart
987 def due_before
991 def due_before
988 due_date || (fixed_version ? fixed_version.effective_date : nil)
992 due_date || (fixed_version ? fixed_version.effective_date : nil)
989 end
993 end
990
994
991 # Returns the time scheduled for this issue.
995 # Returns the time scheduled for this issue.
992 #
996 #
993 # Example:
997 # Example:
994 # Start Date: 2/26/09, End Date: 3/04/09
998 # Start Date: 2/26/09, End Date: 3/04/09
995 # duration => 6
999 # duration => 6
996 def duration
1000 def duration
997 (start_date && due_date) ? due_date - start_date : 0
1001 (start_date && due_date) ? due_date - start_date : 0
998 end
1002 end
999
1003
1000 # Returns the duration in working days
1004 # Returns the duration in working days
1001 def working_duration
1005 def working_duration
1002 (start_date && due_date) ? working_days(start_date, due_date) : 0
1006 (start_date && due_date) ? working_days(start_date, due_date) : 0
1003 end
1007 end
1004
1008
1005 def soonest_start(reload=false)
1009 def soonest_start(reload=false)
1006 @soonest_start = nil if reload
1010 @soonest_start = nil if reload
1007 @soonest_start ||= (
1011 @soonest_start ||= (
1008 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1012 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1009 [(@parent_issue || parent).try(:soonest_start)]
1013 [(@parent_issue || parent).try(:soonest_start)]
1010 ).compact.max
1014 ).compact.max
1011 end
1015 end
1012
1016
1013 # Sets start_date on the given date or the next working day
1017 # Sets start_date on the given date or the next working day
1014 # and changes due_date to keep the same working duration.
1018 # and changes due_date to keep the same working duration.
1015 def reschedule_on(date)
1019 def reschedule_on(date)
1016 wd = working_duration
1020 wd = working_duration
1017 date = next_working_date(date)
1021 date = next_working_date(date)
1018 self.start_date = date
1022 self.start_date = date
1019 self.due_date = add_working_days(date, wd)
1023 self.due_date = add_working_days(date, wd)
1020 end
1024 end
1021
1025
1022 # Reschedules the issue on the given date or the next working day and saves the record.
1026 # Reschedules the issue on the given date or the next working day and saves the record.
1023 # If the issue is a parent task, this is done by rescheduling its subtasks.
1027 # If the issue is a parent task, this is done by rescheduling its subtasks.
1024 def reschedule_on!(date)
1028 def reschedule_on!(date)
1025 return if date.nil?
1029 return if date.nil?
1026 if leaf?
1030 if leaf?
1027 if start_date.nil? || start_date != date
1031 if start_date.nil? || start_date != date
1028 if start_date && start_date > date
1032 if start_date && start_date > date
1029 # Issue can not be moved earlier than its soonest start date
1033 # Issue can not be moved earlier than its soonest start date
1030 date = [soonest_start(true), date].compact.max
1034 date = [soonest_start(true), date].compact.max
1031 end
1035 end
1032 reschedule_on(date)
1036 reschedule_on(date)
1033 begin
1037 begin
1034 save
1038 save
1035 rescue ActiveRecord::StaleObjectError
1039 rescue ActiveRecord::StaleObjectError
1036 reload
1040 reload
1037 reschedule_on(date)
1041 reschedule_on(date)
1038 save
1042 save
1039 end
1043 end
1040 end
1044 end
1041 else
1045 else
1042 leaves.each do |leaf|
1046 leaves.each do |leaf|
1043 if leaf.start_date
1047 if leaf.start_date
1044 # Only move subtask if it starts at the same date as the parent
1048 # Only move subtask if it starts at the same date as the parent
1045 # or if it starts before the given date
1049 # or if it starts before the given date
1046 if start_date == leaf.start_date || date > leaf.start_date
1050 if start_date == leaf.start_date || date > leaf.start_date
1047 leaf.reschedule_on!(date)
1051 leaf.reschedule_on!(date)
1048 end
1052 end
1049 else
1053 else
1050 leaf.reschedule_on!(date)
1054 leaf.reschedule_on!(date)
1051 end
1055 end
1052 end
1056 end
1053 end
1057 end
1054 end
1058 end
1055
1059
1056 def <=>(issue)
1060 def <=>(issue)
1057 if issue.nil?
1061 if issue.nil?
1058 -1
1062 -1
1059 elsif root_id != issue.root_id
1063 elsif root_id != issue.root_id
1060 (root_id || 0) <=> (issue.root_id || 0)
1064 (root_id || 0) <=> (issue.root_id || 0)
1061 else
1065 else
1062 (lft || 0) <=> (issue.lft || 0)
1066 (lft || 0) <=> (issue.lft || 0)
1063 end
1067 end
1064 end
1068 end
1065
1069
1066 def to_s
1070 def to_s
1067 "#{tracker} ##{id}: #{subject}"
1071 "#{tracker} ##{id}: #{subject}"
1068 end
1072 end
1069
1073
1070 # Returns a string of css classes that apply to the issue
1074 # Returns a string of css classes that apply to the issue
1071 def css_classes(user=User.current)
1075 def css_classes(user=User.current)
1072 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1076 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1073 s << ' closed' if closed?
1077 s << ' closed' if closed?
1074 s << ' overdue' if overdue?
1078 s << ' overdue' if overdue?
1075 s << ' child' if child?
1079 s << ' child' if child?
1076 s << ' parent' unless leaf?
1080 s << ' parent' unless leaf?
1077 s << ' private' if is_private?
1081 s << ' private' if is_private?
1078 if user.logged?
1082 if user.logged?
1079 s << ' created-by-me' if author_id == user.id
1083 s << ' created-by-me' if author_id == user.id
1080 s << ' assigned-to-me' if assigned_to_id == user.id
1084 s << ' assigned-to-me' if assigned_to_id == user.id
1081 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id = assigned_to_id}
1085 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id = assigned_to_id}
1082 end
1086 end
1083 s
1087 s
1084 end
1088 end
1085
1089
1086 # Saves an issue and a time_entry from the parameters
1090 # Saves an issue and a time_entry from the parameters
1087 def save_issue_with_child_records(params, existing_time_entry=nil)
1091 def save_issue_with_child_records(params, existing_time_entry=nil)
1088 Issue.transaction do
1092 Issue.transaction do
1089 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
1093 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
1090 @time_entry = existing_time_entry || TimeEntry.new
1094 @time_entry = existing_time_entry || TimeEntry.new
1091 @time_entry.project = project
1095 @time_entry.project = project
1092 @time_entry.issue = self
1096 @time_entry.issue = self
1093 @time_entry.user = User.current
1097 @time_entry.user = User.current
1094 @time_entry.spent_on = User.current.today
1098 @time_entry.spent_on = User.current.today
1095 @time_entry.attributes = params[:time_entry]
1099 @time_entry.attributes = params[:time_entry]
1096 self.time_entries << @time_entry
1100 self.time_entries << @time_entry
1097 end
1101 end
1098
1102
1099 # TODO: Rename hook
1103 # TODO: Rename hook
1100 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
1104 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
1101 if save
1105 if save
1102 # TODO: Rename hook
1106 # TODO: Rename hook
1103 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
1107 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
1104 else
1108 else
1105 raise ActiveRecord::Rollback
1109 raise ActiveRecord::Rollback
1106 end
1110 end
1107 end
1111 end
1108 end
1112 end
1109
1113
1110 # Unassigns issues from +version+ if it's no longer shared with issue's project
1114 # Unassigns issues from +version+ if it's no longer shared with issue's project
1111 def self.update_versions_from_sharing_change(version)
1115 def self.update_versions_from_sharing_change(version)
1112 # Update issues assigned to the version
1116 # Update issues assigned to the version
1113 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1117 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1114 end
1118 end
1115
1119
1116 # Unassigns issues from versions that are no longer shared
1120 # Unassigns issues from versions that are no longer shared
1117 # after +project+ was moved
1121 # after +project+ was moved
1118 def self.update_versions_from_hierarchy_change(project)
1122 def self.update_versions_from_hierarchy_change(project)
1119 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1123 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1120 # Update issues of the moved projects and issues assigned to a version of a moved project
1124 # Update issues of the moved projects and issues assigned to a version of a moved project
1121 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1125 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1122 end
1126 end
1123
1127
1124 def parent_issue_id=(arg)
1128 def parent_issue_id=(arg)
1125 s = arg.to_s.strip.presence
1129 s = arg.to_s.strip.presence
1126 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1130 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1127 @parent_issue.id
1131 @parent_issue.id
1128 else
1132 else
1129 @parent_issue = nil
1133 @parent_issue = nil
1130 @invalid_parent_issue_id = arg
1134 @invalid_parent_issue_id = arg
1131 end
1135 end
1132 end
1136 end
1133
1137
1134 def parent_issue_id
1138 def parent_issue_id
1135 if @invalid_parent_issue_id
1139 if @invalid_parent_issue_id
1136 @invalid_parent_issue_id
1140 @invalid_parent_issue_id
1137 elsif instance_variable_defined? :@parent_issue
1141 elsif instance_variable_defined? :@parent_issue
1138 @parent_issue.nil? ? nil : @parent_issue.id
1142 @parent_issue.nil? ? nil : @parent_issue.id
1139 else
1143 else
1140 parent_id
1144 parent_id
1141 end
1145 end
1142 end
1146 end
1143
1147
1144 # Returns true if issue's project is a valid
1148 # Returns true if issue's project is a valid
1145 # parent issue project
1149 # parent issue project
1146 def valid_parent_project?(issue=parent)
1150 def valid_parent_project?(issue=parent)
1147 return true if issue.nil? || issue.project_id == project_id
1151 return true if issue.nil? || issue.project_id == project_id
1148
1152
1149 case Setting.cross_project_subtasks
1153 case Setting.cross_project_subtasks
1150 when 'system'
1154 when 'system'
1151 true
1155 true
1152 when 'tree'
1156 when 'tree'
1153 issue.project.root == project.root
1157 issue.project.root == project.root
1154 when 'hierarchy'
1158 when 'hierarchy'
1155 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1159 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1156 when 'descendants'
1160 when 'descendants'
1157 issue.project.is_or_is_ancestor_of?(project)
1161 issue.project.is_or_is_ancestor_of?(project)
1158 else
1162 else
1159 false
1163 false
1160 end
1164 end
1161 end
1165 end
1162
1166
1163 # Extracted from the ReportsController.
1167 # Extracted from the ReportsController.
1164 def self.by_tracker(project)
1168 def self.by_tracker(project)
1165 count_and_group_by(:project => project,
1169 count_and_group_by(:project => project,
1166 :field => 'tracker_id',
1170 :field => 'tracker_id',
1167 :joins => Tracker.table_name)
1171 :joins => Tracker.table_name)
1168 end
1172 end
1169
1173
1170 def self.by_version(project)
1174 def self.by_version(project)
1171 count_and_group_by(:project => project,
1175 count_and_group_by(:project => project,
1172 :field => 'fixed_version_id',
1176 :field => 'fixed_version_id',
1173 :joins => Version.table_name)
1177 :joins => Version.table_name)
1174 end
1178 end
1175
1179
1176 def self.by_priority(project)
1180 def self.by_priority(project)
1177 count_and_group_by(:project => project,
1181 count_and_group_by(:project => project,
1178 :field => 'priority_id',
1182 :field => 'priority_id',
1179 :joins => IssuePriority.table_name)
1183 :joins => IssuePriority.table_name)
1180 end
1184 end
1181
1185
1182 def self.by_category(project)
1186 def self.by_category(project)
1183 count_and_group_by(:project => project,
1187 count_and_group_by(:project => project,
1184 :field => 'category_id',
1188 :field => 'category_id',
1185 :joins => IssueCategory.table_name)
1189 :joins => IssueCategory.table_name)
1186 end
1190 end
1187
1191
1188 def self.by_assigned_to(project)
1192 def self.by_assigned_to(project)
1189 count_and_group_by(:project => project,
1193 count_and_group_by(:project => project,
1190 :field => 'assigned_to_id',
1194 :field => 'assigned_to_id',
1191 :joins => User.table_name)
1195 :joins => User.table_name)
1192 end
1196 end
1193
1197
1194 def self.by_author(project)
1198 def self.by_author(project)
1195 count_and_group_by(:project => project,
1199 count_and_group_by(:project => project,
1196 :field => 'author_id',
1200 :field => 'author_id',
1197 :joins => User.table_name)
1201 :joins => User.table_name)
1198 end
1202 end
1199
1203
1200 def self.by_subproject(project)
1204 def self.by_subproject(project)
1201 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1205 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1202 s.is_closed as closed,
1206 s.is_closed as closed,
1203 #{Issue.table_name}.project_id as project_id,
1207 #{Issue.table_name}.project_id as project_id,
1204 count(#{Issue.table_name}.id) as total
1208 count(#{Issue.table_name}.id) as total
1205 from
1209 from
1206 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1210 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1207 where
1211 where
1208 #{Issue.table_name}.status_id=s.id
1212 #{Issue.table_name}.status_id=s.id
1209 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1213 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1210 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1214 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1211 and #{Issue.table_name}.project_id <> #{project.id}
1215 and #{Issue.table_name}.project_id <> #{project.id}
1212 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1216 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1213 end
1217 end
1214 # End ReportsController extraction
1218 # End ReportsController extraction
1215
1219
1216 # Returns a scope of projects that user can assign the issue to
1220 # Returns a scope of projects that user can assign the issue to
1217 def allowed_target_projects(user=User.current)
1221 def allowed_target_projects(user=User.current)
1218 if new_record?
1222 if new_record?
1219 Project.where(Project.allowed_to_condition(user, :add_issues))
1223 Project.where(Project.allowed_to_condition(user, :add_issues))
1220 else
1224 else
1221 self.class.allowed_target_projects_on_move(user)
1225 self.class.allowed_target_projects_on_move(user)
1222 end
1226 end
1223 end
1227 end
1224
1228
1225 # Returns a scope of projects that user can move issues to
1229 # Returns a scope of projects that user can move issues to
1226 def self.allowed_target_projects_on_move(user=User.current)
1230 def self.allowed_target_projects_on_move(user=User.current)
1227 Project.where(Project.allowed_to_condition(user, :move_issues))
1231 Project.where(Project.allowed_to_condition(user, :move_issues))
1228 end
1232 end
1229
1233
1230 private
1234 private
1231
1235
1232 def after_project_change
1236 def after_project_change
1233 # Update project_id on related time entries
1237 # Update project_id on related time entries
1234 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1238 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1235
1239
1236 # Delete issue relations
1240 # Delete issue relations
1237 unless Setting.cross_project_issue_relations?
1241 unless Setting.cross_project_issue_relations?
1238 relations_from.clear
1242 relations_from.clear
1239 relations_to.clear
1243 relations_to.clear
1240 end
1244 end
1241
1245
1242 # Move subtasks that were in the same project
1246 # Move subtasks that were in the same project
1243 children.each do |child|
1247 children.each do |child|
1244 next unless child.project_id == project_id_was
1248 next unless child.project_id == project_id_was
1245 # Change project and keep project
1249 # Change project and keep project
1246 child.send :project=, project, true
1250 child.send :project=, project, true
1247 unless child.save
1251 unless child.save
1248 raise ActiveRecord::Rollback
1252 raise ActiveRecord::Rollback
1249 end
1253 end
1250 end
1254 end
1251 end
1255 end
1252
1256
1253 # Callback for after the creation of an issue by copy
1257 # Callback for after the creation of an issue by copy
1254 # * adds a "copied to" relation with the copied issue
1258 # * adds a "copied to" relation with the copied issue
1255 # * copies subtasks from the copied issue
1259 # * copies subtasks from the copied issue
1256 def after_create_from_copy
1260 def after_create_from_copy
1257 return unless copy? && !@after_create_from_copy_handled
1261 return unless copy? && !@after_create_from_copy_handled
1258
1262
1259 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1263 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1260 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1264 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1261 unless relation.save
1265 unless relation.save
1262 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1266 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1263 end
1267 end
1264 end
1268 end
1265
1269
1266 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1270 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1267 copy_options = (@copy_options || {}).merge(:subtasks => false)
1271 copy_options = (@copy_options || {}).merge(:subtasks => false)
1268 copied_issue_ids = {@copied_from.id => self.id}
1272 copied_issue_ids = {@copied_from.id => self.id}
1269 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1273 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1270 # Do not copy self when copying an issue as a descendant of the copied issue
1274 # Do not copy self when copying an issue as a descendant of the copied issue
1271 next if child == self
1275 next if child == self
1272 # Do not copy subtasks of issues that were not copied
1276 # Do not copy subtasks of issues that were not copied
1273 next unless copied_issue_ids[child.parent_id]
1277 next unless copied_issue_ids[child.parent_id]
1274 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1278 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1275 unless child.visible?
1279 unless child.visible?
1276 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1280 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1277 next
1281 next
1278 end
1282 end
1279 copy = Issue.new.copy_from(child, copy_options)
1283 copy = Issue.new.copy_from(child, copy_options)
1280 copy.author = author
1284 copy.author = author
1281 copy.project = project
1285 copy.project = project
1282 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1286 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1283 unless copy.save
1287 unless copy.save
1284 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1288 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1285 next
1289 next
1286 end
1290 end
1287 copied_issue_ids[child.id] = copy.id
1291 copied_issue_ids[child.id] = copy.id
1288 end
1292 end
1289 end
1293 end
1290 @after_create_from_copy_handled = true
1294 @after_create_from_copy_handled = true
1291 end
1295 end
1292
1296
1293 def update_nested_set_attributes
1297 def update_nested_set_attributes
1294 if root_id.nil?
1298 if root_id.nil?
1295 # issue was just created
1299 # issue was just created
1296 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1300 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1297 set_default_left_and_right
1301 set_default_left_and_right
1298 Issue.update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt], ["id = ?", id])
1302 Issue.update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt], ["id = ?", id])
1299 if @parent_issue
1303 if @parent_issue
1300 move_to_child_of(@parent_issue)
1304 move_to_child_of(@parent_issue)
1301 end
1305 end
1302 elsif parent_issue_id != parent_id
1306 elsif parent_issue_id != parent_id
1303 former_parent_id = parent_id
1307 former_parent_id = parent_id
1304 # moving an existing issue
1308 # moving an existing issue
1305 if @parent_issue && @parent_issue.root_id == root_id
1309 if @parent_issue && @parent_issue.root_id == root_id
1306 # inside the same tree
1310 # inside the same tree
1307 move_to_child_of(@parent_issue)
1311 move_to_child_of(@parent_issue)
1308 else
1312 else
1309 # to another tree
1313 # to another tree
1310 unless root?
1314 unless root?
1311 move_to_right_of(root)
1315 move_to_right_of(root)
1312 end
1316 end
1313 old_root_id = root_id
1317 old_root_id = root_id
1314 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1318 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1315 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1319 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1316 offset = target_maxright + 1 - lft
1320 offset = target_maxright + 1 - lft
1317 Issue.update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset],
1321 Issue.update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset],
1318 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1322 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1319 self[left_column_name] = lft + offset
1323 self[left_column_name] = lft + offset
1320 self[right_column_name] = rgt + offset
1324 self[right_column_name] = rgt + offset
1321 if @parent_issue
1325 if @parent_issue
1322 move_to_child_of(@parent_issue)
1326 move_to_child_of(@parent_issue)
1323 end
1327 end
1324 end
1328 end
1325 # delete invalid relations of all descendants
1329 # delete invalid relations of all descendants
1326 self_and_descendants.each do |issue|
1330 self_and_descendants.each do |issue|
1327 issue.relations.each do |relation|
1331 issue.relations.each do |relation|
1328 relation.destroy unless relation.valid?
1332 relation.destroy unless relation.valid?
1329 end
1333 end
1330 end
1334 end
1331 # update former parent
1335 # update former parent
1332 recalculate_attributes_for(former_parent_id) if former_parent_id
1336 recalculate_attributes_for(former_parent_id) if former_parent_id
1333 end
1337 end
1334 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1338 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1335 end
1339 end
1336
1340
1337 def update_parent_attributes
1341 def update_parent_attributes
1338 recalculate_attributes_for(parent_id) if parent_id
1342 recalculate_attributes_for(parent_id) if parent_id
1339 end
1343 end
1340
1344
1341 def recalculate_attributes_for(issue_id)
1345 def recalculate_attributes_for(issue_id)
1342 if issue_id && p = Issue.find_by_id(issue_id)
1346 if issue_id && p = Issue.find_by_id(issue_id)
1343 # priority = highest priority of children
1347 # priority = highest priority of children
1344 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1348 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1345 p.priority = IssuePriority.find_by_position(priority_position)
1349 p.priority = IssuePriority.find_by_position(priority_position)
1346 end
1350 end
1347
1351
1348 # start/due dates = lowest/highest dates of children
1352 # start/due dates = lowest/highest dates of children
1349 p.start_date = p.children.minimum(:start_date)
1353 p.start_date = p.children.minimum(:start_date)
1350 p.due_date = p.children.maximum(:due_date)
1354 p.due_date = p.children.maximum(:due_date)
1351 if p.start_date && p.due_date && p.due_date < p.start_date
1355 if p.start_date && p.due_date && p.due_date < p.start_date
1352 p.start_date, p.due_date = p.due_date, p.start_date
1356 p.start_date, p.due_date = p.due_date, p.start_date
1353 end
1357 end
1354
1358
1355 # done ratio = weighted average ratio of leaves
1359 # done ratio = weighted average ratio of leaves
1356 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1360 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1357 leaves_count = p.leaves.count
1361 leaves_count = p.leaves.count
1358 if leaves_count > 0
1362 if leaves_count > 0
1359 average = p.leaves.average(:estimated_hours).to_f
1363 average = p.leaves.average(:estimated_hours).to_f
1360 if average == 0
1364 if average == 0
1361 average = 1
1365 average = 1
1362 end
1366 end
1363 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1367 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1364 progress = done / (average * leaves_count)
1368 progress = done / (average * leaves_count)
1365 p.done_ratio = progress.round
1369 p.done_ratio = progress.round
1366 end
1370 end
1367 end
1371 end
1368
1372
1369 # estimate = sum of leaves estimates
1373 # estimate = sum of leaves estimates
1370 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1374 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1371 p.estimated_hours = nil if p.estimated_hours == 0.0
1375 p.estimated_hours = nil if p.estimated_hours == 0.0
1372
1376
1373 # ancestors will be recursively updated
1377 # ancestors will be recursively updated
1374 p.save(:validate => false)
1378 p.save(:validate => false)
1375 end
1379 end
1376 end
1380 end
1377
1381
1378 # Update issues so their versions are not pointing to a
1382 # Update issues so their versions are not pointing to a
1379 # fixed_version that is not shared with the issue's project
1383 # fixed_version that is not shared with the issue's project
1380 def self.update_versions(conditions=nil)
1384 def self.update_versions(conditions=nil)
1381 # Only need to update issues with a fixed_version from
1385 # Only need to update issues with a fixed_version from
1382 # a different project and that is not systemwide shared
1386 # a different project and that is not systemwide shared
1383 Issue.includes(:project, :fixed_version).
1387 Issue.includes(:project, :fixed_version).
1384 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1388 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1385 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1389 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1386 " AND #{Version.table_name}.sharing <> 'system'").
1390 " AND #{Version.table_name}.sharing <> 'system'").
1387 where(conditions).each do |issue|
1391 where(conditions).each do |issue|
1388 next if issue.project.nil? || issue.fixed_version.nil?
1392 next if issue.project.nil? || issue.fixed_version.nil?
1389 unless issue.project.shared_versions.include?(issue.fixed_version)
1393 unless issue.project.shared_versions.include?(issue.fixed_version)
1390 issue.init_journal(User.current)
1394 issue.init_journal(User.current)
1391 issue.fixed_version = nil
1395 issue.fixed_version = nil
1392 issue.save
1396 issue.save
1393 end
1397 end
1394 end
1398 end
1395 end
1399 end
1396
1400
1397 # Callback on file attachment
1401 # Callback on file attachment
1398 def attachment_added(obj)
1402 def attachment_added(obj)
1399 if @current_journal && !obj.new_record?
1403 if @current_journal && !obj.new_record?
1400 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1404 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1401 end
1405 end
1402 end
1406 end
1403
1407
1404 # Callback on attachment deletion
1408 # Callback on attachment deletion
1405 def attachment_removed(obj)
1409 def attachment_removed(obj)
1406 if @current_journal && !obj.new_record?
1410 if @current_journal && !obj.new_record?
1407 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1411 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1408 @current_journal.save
1412 @current_journal.save
1409 end
1413 end
1410 end
1414 end
1411
1415
1412 # Default assignment based on category
1416 # Default assignment based on category
1413 def default_assign
1417 def default_assign
1414 if assigned_to.nil? && category && category.assigned_to
1418 if assigned_to.nil? && category && category.assigned_to
1415 self.assigned_to = category.assigned_to
1419 self.assigned_to = category.assigned_to
1416 end
1420 end
1417 end
1421 end
1418
1422
1419 # Updates start/due dates of following issues
1423 # Updates start/due dates of following issues
1420 def reschedule_following_issues
1424 def reschedule_following_issues
1421 if start_date_changed? || due_date_changed?
1425 if start_date_changed? || due_date_changed?
1422 relations_from.each do |relation|
1426 relations_from.each do |relation|
1423 relation.set_issue_to_dates
1427 relation.set_issue_to_dates
1424 end
1428 end
1425 end
1429 end
1426 end
1430 end
1427
1431
1428 # Closes duplicates if the issue is being closed
1432 # Closes duplicates if the issue is being closed
1429 def close_duplicates
1433 def close_duplicates
1430 if closing?
1434 if closing?
1431 duplicates.each do |duplicate|
1435 duplicates.each do |duplicate|
1432 # Reload is need in case the duplicate was updated by a previous duplicate
1436 # Reload is need in case the duplicate was updated by a previous duplicate
1433 duplicate.reload
1437 duplicate.reload
1434 # Don't re-close it if it's already closed
1438 # Don't re-close it if it's already closed
1435 next if duplicate.closed?
1439 next if duplicate.closed?
1436 # Same user and notes
1440 # Same user and notes
1437 if @current_journal
1441 if @current_journal
1438 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1442 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1439 end
1443 end
1440 duplicate.update_attribute :status, self.status
1444 duplicate.update_attribute :status, self.status
1441 end
1445 end
1442 end
1446 end
1443 end
1447 end
1444
1448
1445 # Make sure updated_on is updated when adding a note and set updated_on now
1449 # Make sure updated_on is updated when adding a note and set updated_on now
1446 # so we can set closed_on with the same value on closing
1450 # so we can set closed_on with the same value on closing
1447 def force_updated_on_change
1451 def force_updated_on_change
1448 if @current_journal || changed?
1452 if @current_journal || changed?
1449 self.updated_on = current_time_from_proper_timezone
1453 self.updated_on = current_time_from_proper_timezone
1450 if new_record?
1454 if new_record?
1451 self.created_on = updated_on
1455 self.created_on = updated_on
1452 end
1456 end
1453 end
1457 end
1454 end
1458 end
1455
1459
1456 # Callback for setting closed_on when the issue is closed.
1460 # Callback for setting closed_on when the issue is closed.
1457 # The closed_on attribute stores the time of the last closing
1461 # The closed_on attribute stores the time of the last closing
1458 # and is preserved when the issue is reopened.
1462 # and is preserved when the issue is reopened.
1459 def update_closed_on
1463 def update_closed_on
1460 if closing? || (new_record? && closed?)
1464 if closing? || (new_record? && closed?)
1461 self.closed_on = updated_on
1465 self.closed_on = updated_on
1462 end
1466 end
1463 end
1467 end
1464
1468
1465 # Saves the changes in a Journal
1469 # Saves the changes in a Journal
1466 # Called after_save
1470 # Called after_save
1467 def create_journal
1471 def create_journal
1468 if @current_journal
1472 if @current_journal
1469 # attributes changes
1473 # attributes changes
1470 if @attributes_before_change
1474 if @attributes_before_change
1471 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1475 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1472 before = @attributes_before_change[c]
1476 before = @attributes_before_change[c]
1473 after = send(c)
1477 after = send(c)
1474 next if before == after || (before.blank? && after.blank?)
1478 next if before == after || (before.blank? && after.blank?)
1475 @current_journal.details << JournalDetail.new(:property => 'attr',
1479 @current_journal.details << JournalDetail.new(:property => 'attr',
1476 :prop_key => c,
1480 :prop_key => c,
1477 :old_value => before,
1481 :old_value => before,
1478 :value => after)
1482 :value => after)
1479 }
1483 }
1480 end
1484 end
1481 if @custom_values_before_change
1485 if @custom_values_before_change
1482 # custom fields changes
1486 # custom fields changes
1483 custom_field_values.each {|c|
1487 custom_field_values.each {|c|
1484 before = @custom_values_before_change[c.custom_field_id]
1488 before = @custom_values_before_change[c.custom_field_id]
1485 after = c.value
1489 after = c.value
1486 next if before == after || (before.blank? && after.blank?)
1490 next if before == after || (before.blank? && after.blank?)
1487
1491
1488 if before.is_a?(Array) || after.is_a?(Array)
1492 if before.is_a?(Array) || after.is_a?(Array)
1489 before = [before] unless before.is_a?(Array)
1493 before = [before] unless before.is_a?(Array)
1490 after = [after] unless after.is_a?(Array)
1494 after = [after] unless after.is_a?(Array)
1491
1495
1492 # values removed
1496 # values removed
1493 (before - after).reject(&:blank?).each do |value|
1497 (before - after).reject(&:blank?).each do |value|
1494 @current_journal.details << JournalDetail.new(:property => 'cf',
1498 @current_journal.details << JournalDetail.new(:property => 'cf',
1495 :prop_key => c.custom_field_id,
1499 :prop_key => c.custom_field_id,
1496 :old_value => value,
1500 :old_value => value,
1497 :value => nil)
1501 :value => nil)
1498 end
1502 end
1499 # values added
1503 # values added
1500 (after - before).reject(&:blank?).each do |value|
1504 (after - before).reject(&:blank?).each do |value|
1501 @current_journal.details << JournalDetail.new(:property => 'cf',
1505 @current_journal.details << JournalDetail.new(:property => 'cf',
1502 :prop_key => c.custom_field_id,
1506 :prop_key => c.custom_field_id,
1503 :old_value => nil,
1507 :old_value => nil,
1504 :value => value)
1508 :value => value)
1505 end
1509 end
1506 else
1510 else
1507 @current_journal.details << JournalDetail.new(:property => 'cf',
1511 @current_journal.details << JournalDetail.new(:property => 'cf',
1508 :prop_key => c.custom_field_id,
1512 :prop_key => c.custom_field_id,
1509 :old_value => before,
1513 :old_value => before,
1510 :value => after)
1514 :value => after)
1511 end
1515 end
1512 }
1516 }
1513 end
1517 end
1514 @current_journal.save
1518 @current_journal.save
1515 # reset current journal
1519 # reset current journal
1516 init_journal @current_journal.user, @current_journal.notes
1520 init_journal @current_journal.user, @current_journal.notes
1517 end
1521 end
1518 end
1522 end
1519
1523
1520 def send_notification
1524 def send_notification
1521 if Setting.notified_events.include?('issue_added')
1525 if Setting.notified_events.include?('issue_added')
1522 Mailer.deliver_issue_add(self)
1526 Mailer.deliver_issue_add(self)
1523 end
1527 end
1524 end
1528 end
1525
1529
1526 # Query generator for selecting groups of issue counts for a project
1530 # Query generator for selecting groups of issue counts for a project
1527 # based on specific criteria
1531 # based on specific criteria
1528 #
1532 #
1529 # Options
1533 # Options
1530 # * project - Project to search in.
1534 # * project - Project to search in.
1531 # * field - String. Issue field to key off of in the grouping.
1535 # * field - String. Issue field to key off of in the grouping.
1532 # * joins - String. The table name to join against.
1536 # * joins - String. The table name to join against.
1533 def self.count_and_group_by(options)
1537 def self.count_and_group_by(options)
1534 project = options.delete(:project)
1538 project = options.delete(:project)
1535 select_field = options.delete(:field)
1539 select_field = options.delete(:field)
1536 joins = options.delete(:joins)
1540 joins = options.delete(:joins)
1537
1541
1538 where = "#{Issue.table_name}.#{select_field}=j.id"
1542 where = "#{Issue.table_name}.#{select_field}=j.id"
1539
1543
1540 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1544 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1541 s.is_closed as closed,
1545 s.is_closed as closed,
1542 j.id as #{select_field},
1546 j.id as #{select_field},
1543 count(#{Issue.table_name}.id) as total
1547 count(#{Issue.table_name}.id) as total
1544 from
1548 from
1545 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1549 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1546 where
1550 where
1547 #{Issue.table_name}.status_id=s.id
1551 #{Issue.table_name}.status_id=s.id
1548 and #{where}
1552 and #{where}
1549 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1553 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1550 and #{visible_condition(User.current, :project => project)}
1554 and #{visible_condition(User.current, :project => project)}
1551 group by s.id, s.is_closed, j.id")
1555 group by s.id, s.is_closed, j.id")
1552 end
1556 end
1553 end
1557 end
@@ -1,2270 +1,2290
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues, :journals, :journal_details,
28 :issues, :journals, :journal_details,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 include Redmine::I18n
32 include Redmine::I18n
33
33
34 def teardown
34 def teardown
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_initialize
38 def test_initialize
39 issue = Issue.new
39 issue = Issue.new
40
40
41 assert_nil issue.project_id
41 assert_nil issue.project_id
42 assert_nil issue.tracker_id
42 assert_nil issue.tracker_id
43 assert_nil issue.author_id
43 assert_nil issue.author_id
44 assert_nil issue.assigned_to_id
44 assert_nil issue.assigned_to_id
45 assert_nil issue.category_id
45 assert_nil issue.category_id
46
46
47 assert_equal IssueStatus.default, issue.status
47 assert_equal IssueStatus.default, issue.status
48 assert_equal IssuePriority.default, issue.priority
48 assert_equal IssuePriority.default, issue.priority
49 end
49 end
50
50
51 def test_create
51 def test_create
52 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
52 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
53 :status_id => 1, :priority => IssuePriority.all.first,
53 :status_id => 1, :priority => IssuePriority.all.first,
54 :subject => 'test_create',
54 :subject => 'test_create',
55 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
55 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
56 assert issue.save
56 assert issue.save
57 issue.reload
57 issue.reload
58 assert_equal 1.5, issue.estimated_hours
58 assert_equal 1.5, issue.estimated_hours
59 end
59 end
60
60
61 def test_create_minimal
61 def test_create_minimal
62 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
62 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
63 :status_id => 1, :priority => IssuePriority.all.first,
63 :status_id => 1, :priority => IssuePriority.all.first,
64 :subject => 'test_create')
64 :subject => 'test_create')
65 assert issue.save
65 assert issue.save
66 assert issue.description.nil?
66 assert issue.description.nil?
67 assert_nil issue.estimated_hours
67 assert_nil issue.estimated_hours
68 end
68 end
69
69
70 def test_start_date_format_should_be_validated
70 def test_start_date_format_should_be_validated
71 set_language_if_valid 'en'
71 set_language_if_valid 'en'
72 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
72 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
73 issue = Issue.new(:start_date => invalid_date)
73 issue = Issue.new(:start_date => invalid_date)
74 assert !issue.valid?
74 assert !issue.valid?
75 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
75 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
76 end
76 end
77 end
77 end
78
78
79 def test_due_date_format_should_be_validated
79 def test_due_date_format_should_be_validated
80 set_language_if_valid 'en'
80 set_language_if_valid 'en'
81 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
81 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
82 issue = Issue.new(:due_date => invalid_date)
82 issue = Issue.new(:due_date => invalid_date)
83 assert !issue.valid?
83 assert !issue.valid?
84 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
84 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
85 end
85 end
86 end
86 end
87
87
88 def test_due_date_lesser_than_start_date_should_not_validate
88 def test_due_date_lesser_than_start_date_should_not_validate
89 set_language_if_valid 'en'
89 set_language_if_valid 'en'
90 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
90 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
91 assert !issue.valid?
91 assert !issue.valid?
92 assert_include 'Due date must be greater than start date', issue.errors.full_messages
92 assert_include 'Due date must be greater than start date', issue.errors.full_messages
93 end
93 end
94
94
95 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
95 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
96 issue = Issue.generate(:start_date => '2013-06-04')
96 issue = Issue.generate(:start_date => '2013-06-04')
97 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
97 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
98 assert !issue.valid?
98 assert !issue.valid?
99 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
99 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
100 end
100 end
101
101
102 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
102 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
103 issue = Issue.generate!(:start_date => '2013-06-04')
103 issue = Issue.generate!(:start_date => '2013-06-04')
104 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
104 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
105 issue.start_date = '2013-06-07'
105 issue.start_date = '2013-06-07'
106 assert !issue.valid?
106 assert !issue.valid?
107 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
107 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
108 end
108 end
109
109
110 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
110 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
111 issue = Issue.generate!(:start_date => '2013-06-04')
111 issue = Issue.generate!(:start_date => '2013-06-04')
112 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
112 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
113 assert issue.valid?
113 assert issue.valid?
114 end
114 end
115
115
116 def test_estimated_hours_should_be_validated
116 def test_estimated_hours_should_be_validated
117 set_language_if_valid 'en'
117 set_language_if_valid 'en'
118 ['-2'].each do |invalid|
118 ['-2'].each do |invalid|
119 issue = Issue.new(:estimated_hours => invalid)
119 issue = Issue.new(:estimated_hours => invalid)
120 assert !issue.valid?
120 assert !issue.valid?
121 assert_include 'Estimated time is invalid', issue.errors.full_messages
121 assert_include 'Estimated time is invalid', issue.errors.full_messages
122 end
122 end
123 end
123 end
124
124
125 def test_create_with_required_custom_field
125 def test_create_with_required_custom_field
126 set_language_if_valid 'en'
126 set_language_if_valid 'en'
127 field = IssueCustomField.find_by_name('Database')
127 field = IssueCustomField.find_by_name('Database')
128 field.update_attribute(:is_required, true)
128 field.update_attribute(:is_required, true)
129
129
130 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
130 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
131 :status_id => 1, :subject => 'test_create',
131 :status_id => 1, :subject => 'test_create',
132 :description => 'IssueTest#test_create_with_required_custom_field')
132 :description => 'IssueTest#test_create_with_required_custom_field')
133 assert issue.available_custom_fields.include?(field)
133 assert issue.available_custom_fields.include?(field)
134 # No value for the custom field
134 # No value for the custom field
135 assert !issue.save
135 assert !issue.save
136 assert_equal ["Database can't be blank"], issue.errors.full_messages
136 assert_equal ["Database can't be blank"], issue.errors.full_messages
137 # Blank value
137 # Blank value
138 issue.custom_field_values = { field.id => '' }
138 issue.custom_field_values = { field.id => '' }
139 assert !issue.save
139 assert !issue.save
140 assert_equal ["Database can't be blank"], issue.errors.full_messages
140 assert_equal ["Database can't be blank"], issue.errors.full_messages
141 # Invalid value
141 # Invalid value
142 issue.custom_field_values = { field.id => 'SQLServer' }
142 issue.custom_field_values = { field.id => 'SQLServer' }
143 assert !issue.save
143 assert !issue.save
144 assert_equal ["Database is not included in the list"], issue.errors.full_messages
144 assert_equal ["Database is not included in the list"], issue.errors.full_messages
145 # Valid value
145 # Valid value
146 issue.custom_field_values = { field.id => 'PostgreSQL' }
146 issue.custom_field_values = { field.id => 'PostgreSQL' }
147 assert issue.save
147 assert issue.save
148 issue.reload
148 issue.reload
149 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
149 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
150 end
150 end
151
151
152 def test_create_with_group_assignment
152 def test_create_with_group_assignment
153 with_settings :issue_group_assignment => '1' do
153 with_settings :issue_group_assignment => '1' do
154 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
154 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
155 :subject => 'Group assignment',
155 :subject => 'Group assignment',
156 :assigned_to_id => 11).save
156 :assigned_to_id => 11).save
157 issue = Issue.first(:order => 'id DESC')
157 issue = Issue.first(:order => 'id DESC')
158 assert_kind_of Group, issue.assigned_to
158 assert_kind_of Group, issue.assigned_to
159 assert_equal Group.find(11), issue.assigned_to
159 assert_equal Group.find(11), issue.assigned_to
160 end
160 end
161 end
161 end
162
162
163 def test_create_with_parent_issue_id
163 def test_create_with_parent_issue_id
164 issue = Issue.new(:project_id => 1, :tracker_id => 1,
164 issue = Issue.new(:project_id => 1, :tracker_id => 1,
165 :author_id => 1, :subject => 'Group assignment',
165 :author_id => 1, :subject => 'Group assignment',
166 :parent_issue_id => 1)
166 :parent_issue_id => 1)
167 assert_save issue
167 assert_save issue
168 assert_equal 1, issue.parent_issue_id
168 assert_equal 1, issue.parent_issue_id
169 assert_equal Issue.find(1), issue.parent
169 assert_equal Issue.find(1), issue.parent
170 end
170 end
171
171
172 def test_create_with_sharp_parent_issue_id
172 def test_create_with_sharp_parent_issue_id
173 issue = Issue.new(:project_id => 1, :tracker_id => 1,
173 issue = Issue.new(:project_id => 1, :tracker_id => 1,
174 :author_id => 1, :subject => 'Group assignment',
174 :author_id => 1, :subject => 'Group assignment',
175 :parent_issue_id => "#1")
175 :parent_issue_id => "#1")
176 assert_save issue
176 assert_save issue
177 assert_equal 1, issue.parent_issue_id
177 assert_equal 1, issue.parent_issue_id
178 assert_equal Issue.find(1), issue.parent
178 assert_equal Issue.find(1), issue.parent
179 end
179 end
180
180
181 def test_create_with_invalid_parent_issue_id
181 def test_create_with_invalid_parent_issue_id
182 set_language_if_valid 'en'
182 set_language_if_valid 'en'
183 issue = Issue.new(:project_id => 1, :tracker_id => 1,
183 issue = Issue.new(:project_id => 1, :tracker_id => 1,
184 :author_id => 1, :subject => 'Group assignment',
184 :author_id => 1, :subject => 'Group assignment',
185 :parent_issue_id => '01ABC')
185 :parent_issue_id => '01ABC')
186 assert !issue.save
186 assert !issue.save
187 assert_equal '01ABC', issue.parent_issue_id
187 assert_equal '01ABC', issue.parent_issue_id
188 assert_include 'Parent task is invalid', issue.errors.full_messages
188 assert_include 'Parent task is invalid', issue.errors.full_messages
189 end
189 end
190
190
191 def test_create_with_invalid_sharp_parent_issue_id
191 def test_create_with_invalid_sharp_parent_issue_id
192 set_language_if_valid 'en'
192 set_language_if_valid 'en'
193 issue = Issue.new(:project_id => 1, :tracker_id => 1,
193 issue = Issue.new(:project_id => 1, :tracker_id => 1,
194 :author_id => 1, :subject => 'Group assignment',
194 :author_id => 1, :subject => 'Group assignment',
195 :parent_issue_id => '#01ABC')
195 :parent_issue_id => '#01ABC')
196 assert !issue.save
196 assert !issue.save
197 assert_equal '#01ABC', issue.parent_issue_id
197 assert_equal '#01ABC', issue.parent_issue_id
198 assert_include 'Parent task is invalid', issue.errors.full_messages
198 assert_include 'Parent task is invalid', issue.errors.full_messages
199 end
199 end
200
200
201 def assert_visibility_match(user, issues)
201 def assert_visibility_match(user, issues)
202 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
202 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
203 end
203 end
204
204
205 def test_visible_scope_for_anonymous
205 def test_visible_scope_for_anonymous
206 # Anonymous user should see issues of public projects only
206 # Anonymous user should see issues of public projects only
207 issues = Issue.visible(User.anonymous).all
207 issues = Issue.visible(User.anonymous).all
208 assert issues.any?
208 assert issues.any?
209 assert_nil issues.detect {|issue| !issue.project.is_public?}
209 assert_nil issues.detect {|issue| !issue.project.is_public?}
210 assert_nil issues.detect {|issue| issue.is_private?}
210 assert_nil issues.detect {|issue| issue.is_private?}
211 assert_visibility_match User.anonymous, issues
211 assert_visibility_match User.anonymous, issues
212 end
212 end
213
213
214 def test_visible_scope_for_anonymous_without_view_issues_permissions
214 def test_visible_scope_for_anonymous_without_view_issues_permissions
215 # Anonymous user should not see issues without permission
215 # Anonymous user should not see issues without permission
216 Role.anonymous.remove_permission!(:view_issues)
216 Role.anonymous.remove_permission!(:view_issues)
217 issues = Issue.visible(User.anonymous).all
217 issues = Issue.visible(User.anonymous).all
218 assert issues.empty?
218 assert issues.empty?
219 assert_visibility_match User.anonymous, issues
219 assert_visibility_match User.anonymous, issues
220 end
220 end
221
221
222 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
222 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
223 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
223 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
224 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
224 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
225 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
225 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
226 assert !issue.visible?(User.anonymous)
226 assert !issue.visible?(User.anonymous)
227 end
227 end
228
228
229 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
229 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
230 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
230 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
231 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
231 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
232 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
232 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
233 assert !issue.visible?(User.anonymous)
233 assert !issue.visible?(User.anonymous)
234 end
234 end
235
235
236 def test_visible_scope_for_non_member
236 def test_visible_scope_for_non_member
237 user = User.find(9)
237 user = User.find(9)
238 assert user.projects.empty?
238 assert user.projects.empty?
239 # Non member user should see issues of public projects only
239 # Non member user should see issues of public projects only
240 issues = Issue.visible(user).all
240 issues = Issue.visible(user).all
241 assert issues.any?
241 assert issues.any?
242 assert_nil issues.detect {|issue| !issue.project.is_public?}
242 assert_nil issues.detect {|issue| !issue.project.is_public?}
243 assert_nil issues.detect {|issue| issue.is_private?}
243 assert_nil issues.detect {|issue| issue.is_private?}
244 assert_visibility_match user, issues
244 assert_visibility_match user, issues
245 end
245 end
246
246
247 def test_visible_scope_for_non_member_with_own_issues_visibility
247 def test_visible_scope_for_non_member_with_own_issues_visibility
248 Role.non_member.update_attribute :issues_visibility, 'own'
248 Role.non_member.update_attribute :issues_visibility, 'own'
249 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
249 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
250 user = User.find(9)
250 user = User.find(9)
251
251
252 issues = Issue.visible(user).all
252 issues = Issue.visible(user).all
253 assert issues.any?
253 assert issues.any?
254 assert_nil issues.detect {|issue| issue.author != user}
254 assert_nil issues.detect {|issue| issue.author != user}
255 assert_visibility_match user, issues
255 assert_visibility_match user, issues
256 end
256 end
257
257
258 def test_visible_scope_for_non_member_without_view_issues_permissions
258 def test_visible_scope_for_non_member_without_view_issues_permissions
259 # Non member user should not see issues without permission
259 # Non member user should not see issues without permission
260 Role.non_member.remove_permission!(:view_issues)
260 Role.non_member.remove_permission!(:view_issues)
261 user = User.find(9)
261 user = User.find(9)
262 assert user.projects.empty?
262 assert user.projects.empty?
263 issues = Issue.visible(user).all
263 issues = Issue.visible(user).all
264 assert issues.empty?
264 assert issues.empty?
265 assert_visibility_match user, issues
265 assert_visibility_match user, issues
266 end
266 end
267
267
268 def test_visible_scope_for_member
268 def test_visible_scope_for_member
269 user = User.find(9)
269 user = User.find(9)
270 # User should see issues of projects for which user has view_issues permissions only
270 # User should see issues of projects for which user has view_issues permissions only
271 Role.non_member.remove_permission!(:view_issues)
271 Role.non_member.remove_permission!(:view_issues)
272 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
272 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
273 issues = Issue.visible(user).all
273 issues = Issue.visible(user).all
274 assert issues.any?
274 assert issues.any?
275 assert_nil issues.detect {|issue| issue.project_id != 3}
275 assert_nil issues.detect {|issue| issue.project_id != 3}
276 assert_nil issues.detect {|issue| issue.is_private?}
276 assert_nil issues.detect {|issue| issue.is_private?}
277 assert_visibility_match user, issues
277 assert_visibility_match user, issues
278 end
278 end
279
279
280 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
280 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
281 user = User.find(8)
281 user = User.find(8)
282 assert user.groups.any?
282 assert user.groups.any?
283 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
283 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
284 Role.non_member.remove_permission!(:view_issues)
284 Role.non_member.remove_permission!(:view_issues)
285
285
286 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
286 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
287 :status_id => 1, :priority => IssuePriority.all.first,
287 :status_id => 1, :priority => IssuePriority.all.first,
288 :subject => 'Assignment test',
288 :subject => 'Assignment test',
289 :assigned_to => user.groups.first,
289 :assigned_to => user.groups.first,
290 :is_private => true)
290 :is_private => true)
291
291
292 Role.find(2).update_attribute :issues_visibility, 'default'
292 Role.find(2).update_attribute :issues_visibility, 'default'
293 issues = Issue.visible(User.find(8)).all
293 issues = Issue.visible(User.find(8)).all
294 assert issues.any?
294 assert issues.any?
295 assert issues.include?(issue)
295 assert issues.include?(issue)
296
296
297 Role.find(2).update_attribute :issues_visibility, 'own'
297 Role.find(2).update_attribute :issues_visibility, 'own'
298 issues = Issue.visible(User.find(8)).all
298 issues = Issue.visible(User.find(8)).all
299 assert issues.any?
299 assert issues.any?
300 assert issues.include?(issue)
300 assert issues.include?(issue)
301 end
301 end
302
302
303 def test_visible_scope_for_admin
303 def test_visible_scope_for_admin
304 user = User.find(1)
304 user = User.find(1)
305 user.members.each(&:destroy)
305 user.members.each(&:destroy)
306 assert user.projects.empty?
306 assert user.projects.empty?
307 issues = Issue.visible(user).all
307 issues = Issue.visible(user).all
308 assert issues.any?
308 assert issues.any?
309 # Admin should see issues on private projects that admin does not belong to
309 # Admin should see issues on private projects that admin does not belong to
310 assert issues.detect {|issue| !issue.project.is_public?}
310 assert issues.detect {|issue| !issue.project.is_public?}
311 # Admin should see private issues of other users
311 # Admin should see private issues of other users
312 assert issues.detect {|issue| issue.is_private? && issue.author != user}
312 assert issues.detect {|issue| issue.is_private? && issue.author != user}
313 assert_visibility_match user, issues
313 assert_visibility_match user, issues
314 end
314 end
315
315
316 def test_visible_scope_with_project
316 def test_visible_scope_with_project
317 project = Project.find(1)
317 project = Project.find(1)
318 issues = Issue.visible(User.find(2), :project => project).all
318 issues = Issue.visible(User.find(2), :project => project).all
319 projects = issues.collect(&:project).uniq
319 projects = issues.collect(&:project).uniq
320 assert_equal 1, projects.size
320 assert_equal 1, projects.size
321 assert_equal project, projects.first
321 assert_equal project, projects.first
322 end
322 end
323
323
324 def test_visible_scope_with_project_and_subprojects
324 def test_visible_scope_with_project_and_subprojects
325 project = Project.find(1)
325 project = Project.find(1)
326 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
326 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
327 projects = issues.collect(&:project).uniq
327 projects = issues.collect(&:project).uniq
328 assert projects.size > 1
328 assert projects.size > 1
329 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
329 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
330 end
330 end
331
331
332 def test_visible_and_nested_set_scopes
332 def test_visible_and_nested_set_scopes
333 assert_equal 0, Issue.find(1).descendants.visible.all.size
333 assert_equal 0, Issue.find(1).descendants.visible.all.size
334 end
334 end
335
335
336 def test_open_scope
336 def test_open_scope
337 issues = Issue.open.all
337 issues = Issue.open.all
338 assert_nil issues.detect(&:closed?)
338 assert_nil issues.detect(&:closed?)
339 end
339 end
340
340
341 def test_open_scope_with_arg
341 def test_open_scope_with_arg
342 issues = Issue.open(false).all
342 issues = Issue.open(false).all
343 assert_equal issues, issues.select(&:closed?)
343 assert_equal issues, issues.select(&:closed?)
344 end
344 end
345
345
346 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
346 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
347 version = Version.find(2)
347 version = Version.find(2)
348 assert version.fixed_issues.any?
348 assert version.fixed_issues.any?
349 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
349 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
350 end
350 end
351
351
352 def test_fixed_version_scope_with_empty_array_should_return_no_result
352 def test_fixed_version_scope_with_empty_array_should_return_no_result
353 assert_equal 0, Issue.fixed_version([]).count
353 assert_equal 0, Issue.fixed_version([]).count
354 end
354 end
355
355
356 def test_errors_full_messages_should_include_custom_fields_errors
356 def test_errors_full_messages_should_include_custom_fields_errors
357 field = IssueCustomField.find_by_name('Database')
357 field = IssueCustomField.find_by_name('Database')
358
358
359 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
359 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
360 :status_id => 1, :subject => 'test_create',
360 :status_id => 1, :subject => 'test_create',
361 :description => 'IssueTest#test_create_with_required_custom_field')
361 :description => 'IssueTest#test_create_with_required_custom_field')
362 assert issue.available_custom_fields.include?(field)
362 assert issue.available_custom_fields.include?(field)
363 # Invalid value
363 # Invalid value
364 issue.custom_field_values = { field.id => 'SQLServer' }
364 issue.custom_field_values = { field.id => 'SQLServer' }
365
365
366 assert !issue.valid?
366 assert !issue.valid?
367 assert_equal 1, issue.errors.full_messages.size
367 assert_equal 1, issue.errors.full_messages.size
368 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
368 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
369 issue.errors.full_messages.first
369 issue.errors.full_messages.first
370 end
370 end
371
371
372 def test_update_issue_with_required_custom_field
372 def test_update_issue_with_required_custom_field
373 field = IssueCustomField.find_by_name('Database')
373 field = IssueCustomField.find_by_name('Database')
374 field.update_attribute(:is_required, true)
374 field.update_attribute(:is_required, true)
375
375
376 issue = Issue.find(1)
376 issue = Issue.find(1)
377 assert_nil issue.custom_value_for(field)
377 assert_nil issue.custom_value_for(field)
378 assert issue.available_custom_fields.include?(field)
378 assert issue.available_custom_fields.include?(field)
379 # No change to custom values, issue can be saved
379 # No change to custom values, issue can be saved
380 assert issue.save
380 assert issue.save
381 # Blank value
381 # Blank value
382 issue.custom_field_values = { field.id => '' }
382 issue.custom_field_values = { field.id => '' }
383 assert !issue.save
383 assert !issue.save
384 # Valid value
384 # Valid value
385 issue.custom_field_values = { field.id => 'PostgreSQL' }
385 issue.custom_field_values = { field.id => 'PostgreSQL' }
386 assert issue.save
386 assert issue.save
387 issue.reload
387 issue.reload
388 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
388 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
389 end
389 end
390
390
391 def test_should_not_update_attributes_if_custom_fields_validation_fails
391 def test_should_not_update_attributes_if_custom_fields_validation_fails
392 issue = Issue.find(1)
392 issue = Issue.find(1)
393 field = IssueCustomField.find_by_name('Database')
393 field = IssueCustomField.find_by_name('Database')
394 assert issue.available_custom_fields.include?(field)
394 assert issue.available_custom_fields.include?(field)
395
395
396 issue.custom_field_values = { field.id => 'Invalid' }
396 issue.custom_field_values = { field.id => 'Invalid' }
397 issue.subject = 'Should be not be saved'
397 issue.subject = 'Should be not be saved'
398 assert !issue.save
398 assert !issue.save
399
399
400 issue.reload
400 issue.reload
401 assert_equal "Can't print recipes", issue.subject
401 assert_equal "Can't print recipes", issue.subject
402 end
402 end
403
403
404 def test_should_not_recreate_custom_values_objects_on_update
404 def test_should_not_recreate_custom_values_objects_on_update
405 field = IssueCustomField.find_by_name('Database')
405 field = IssueCustomField.find_by_name('Database')
406
406
407 issue = Issue.find(1)
407 issue = Issue.find(1)
408 issue.custom_field_values = { field.id => 'PostgreSQL' }
408 issue.custom_field_values = { field.id => 'PostgreSQL' }
409 assert issue.save
409 assert issue.save
410 custom_value = issue.custom_value_for(field)
410 custom_value = issue.custom_value_for(field)
411 issue.reload
411 issue.reload
412 issue.custom_field_values = { field.id => 'MySQL' }
412 issue.custom_field_values = { field.id => 'MySQL' }
413 assert issue.save
413 assert issue.save
414 issue.reload
414 issue.reload
415 assert_equal custom_value.id, issue.custom_value_for(field).id
415 assert_equal custom_value.id, issue.custom_value_for(field).id
416 end
416 end
417
417
418 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
418 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
419 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
419 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
420 :status_id => 1, :subject => 'Test',
420 :status_id => 1, :subject => 'Test',
421 :custom_field_values => {'2' => 'Test'})
421 :custom_field_values => {'2' => 'Test'})
422 assert !Tracker.find(2).custom_field_ids.include?(2)
422 assert !Tracker.find(2).custom_field_ids.include?(2)
423
423
424 issue = Issue.find(issue.id)
424 issue = Issue.find(issue.id)
425 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
425 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
426
426
427 issue = Issue.find(issue.id)
427 issue = Issue.find(issue.id)
428 custom_value = issue.custom_value_for(2)
428 custom_value = issue.custom_value_for(2)
429 assert_not_nil custom_value
429 assert_not_nil custom_value
430 assert_equal 'Test', custom_value.value
430 assert_equal 'Test', custom_value.value
431 end
431 end
432
432
433 def test_assigning_tracker_id_should_reload_custom_fields_values
433 def test_assigning_tracker_id_should_reload_custom_fields_values
434 issue = Issue.new(:project => Project.find(1))
434 issue = Issue.new(:project => Project.find(1))
435 assert issue.custom_field_values.empty?
435 assert issue.custom_field_values.empty?
436 issue.tracker_id = 1
436 issue.tracker_id = 1
437 assert issue.custom_field_values.any?
437 assert issue.custom_field_values.any?
438 end
438 end
439
439
440 def test_assigning_attributes_should_assign_project_and_tracker_first
440 def test_assigning_attributes_should_assign_project_and_tracker_first
441 seq = sequence('seq')
441 seq = sequence('seq')
442 issue = Issue.new
442 issue = Issue.new
443 issue.expects(:project_id=).in_sequence(seq)
443 issue.expects(:project_id=).in_sequence(seq)
444 issue.expects(:tracker_id=).in_sequence(seq)
444 issue.expects(:tracker_id=).in_sequence(seq)
445 issue.expects(:subject=).in_sequence(seq)
445 issue.expects(:subject=).in_sequence(seq)
446 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
446 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
447 end
447 end
448
448
449 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
449 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
450 attributes = ActiveSupport::OrderedHash.new
450 attributes = ActiveSupport::OrderedHash.new
451 attributes['custom_field_values'] = { '1' => 'MySQL' }
451 attributes['custom_field_values'] = { '1' => 'MySQL' }
452 attributes['tracker_id'] = '1'
452 attributes['tracker_id'] = '1'
453 issue = Issue.new(:project => Project.find(1))
453 issue = Issue.new(:project => Project.find(1))
454 issue.attributes = attributes
454 issue.attributes = attributes
455 assert_equal 'MySQL', issue.custom_field_value(1)
455 assert_equal 'MySQL', issue.custom_field_value(1)
456 end
456 end
457
457
458 def test_reload_should_reload_custom_field_values
458 def test_reload_should_reload_custom_field_values
459 issue = Issue.generate!
459 issue = Issue.generate!
460 issue.custom_field_values = {'2' => 'Foo'}
460 issue.custom_field_values = {'2' => 'Foo'}
461 issue.save!
461 issue.save!
462
462
463 issue = Issue.order('id desc').first
463 issue = Issue.order('id desc').first
464 assert_equal 'Foo', issue.custom_field_value(2)
464 assert_equal 'Foo', issue.custom_field_value(2)
465
465
466 issue.custom_field_values = {'2' => 'Bar'}
466 issue.custom_field_values = {'2' => 'Bar'}
467 assert_equal 'Bar', issue.custom_field_value(2)
467 assert_equal 'Bar', issue.custom_field_value(2)
468
468
469 issue.reload
469 issue.reload
470 assert_equal 'Foo', issue.custom_field_value(2)
470 assert_equal 'Foo', issue.custom_field_value(2)
471 end
471 end
472
472
473 def test_should_update_issue_with_disabled_tracker
473 def test_should_update_issue_with_disabled_tracker
474 p = Project.find(1)
474 p = Project.find(1)
475 issue = Issue.find(1)
475 issue = Issue.find(1)
476
476
477 p.trackers.delete(issue.tracker)
477 p.trackers.delete(issue.tracker)
478 assert !p.trackers.include?(issue.tracker)
478 assert !p.trackers.include?(issue.tracker)
479
479
480 issue.reload
480 issue.reload
481 issue.subject = 'New subject'
481 issue.subject = 'New subject'
482 assert issue.save
482 assert issue.save
483 end
483 end
484
484
485 def test_should_not_set_a_disabled_tracker
485 def test_should_not_set_a_disabled_tracker
486 p = Project.find(1)
486 p = Project.find(1)
487 p.trackers.delete(Tracker.find(2))
487 p.trackers.delete(Tracker.find(2))
488
488
489 issue = Issue.find(1)
489 issue = Issue.find(1)
490 issue.tracker_id = 2
490 issue.tracker_id = 2
491 issue.subject = 'New subject'
491 issue.subject = 'New subject'
492 assert !issue.save
492 assert !issue.save
493 assert_not_nil issue.errors[:tracker_id]
493 assert_not_nil issue.errors[:tracker_id]
494 end
494 end
495
495
496 def test_category_based_assignment
496 def test_category_based_assignment
497 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
497 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
498 :status_id => 1, :priority => IssuePriority.all.first,
498 :status_id => 1, :priority => IssuePriority.all.first,
499 :subject => 'Assignment test',
499 :subject => 'Assignment test',
500 :description => 'Assignment test', :category_id => 1)
500 :description => 'Assignment test', :category_id => 1)
501 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
501 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
502 end
502 end
503
503
504 def test_new_statuses_allowed_to
504 def test_new_statuses_allowed_to
505 WorkflowTransition.delete_all
505 WorkflowTransition.delete_all
506 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
506 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
507 :old_status_id => 1, :new_status_id => 2,
507 :old_status_id => 1, :new_status_id => 2,
508 :author => false, :assignee => false)
508 :author => false, :assignee => false)
509 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
509 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
510 :old_status_id => 1, :new_status_id => 3,
510 :old_status_id => 1, :new_status_id => 3,
511 :author => true, :assignee => false)
511 :author => true, :assignee => false)
512 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
512 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
513 :new_status_id => 4, :author => false,
513 :old_status_id => 1, :new_status_id => 4,
514 :assignee => true)
514 :author => false, :assignee => true)
515 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
515 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
516 :old_status_id => 1, :new_status_id => 5,
516 :old_status_id => 1, :new_status_id => 5,
517 :author => true, :assignee => true)
517 :author => true, :assignee => true)
518 status = IssueStatus.find(1)
518 status = IssueStatus.find(1)
519 role = Role.find(1)
519 role = Role.find(1)
520 tracker = Tracker.find(1)
520 tracker = Tracker.find(1)
521 user = User.find(2)
521 user = User.find(2)
522
522
523 issue = Issue.generate!(:tracker => tracker, :status => status,
523 issue = Issue.generate!(:tracker => tracker, :status => status,
524 :project_id => 1, :author_id => 1)
524 :project_id => 1, :author_id => 1)
525 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
525 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
526
526
527 issue = Issue.generate!(:tracker => tracker, :status => status,
527 issue = Issue.generate!(:tracker => tracker, :status => status,
528 :project_id => 1, :author => user)
528 :project_id => 1, :author => user)
529 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
529 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
530
530
531 issue = Issue.generate!(:tracker => tracker, :status => status,
531 issue = Issue.generate!(:tracker => tracker, :status => status,
532 :project_id => 1, :author_id => 1,
532 :project_id => 1, :author_id => 1,
533 :assigned_to => user)
533 :assigned_to => user)
534 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
534 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
535
535
536 issue = Issue.generate!(:tracker => tracker, :status => status,
536 issue = Issue.generate!(:tracker => tracker, :status => status,
537 :project_id => 1, :author => user,
537 :project_id => 1, :author => user,
538 :assigned_to => user)
538 :assigned_to => user)
539 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
539 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
540
541 group = Group.generate!
542 group.users << user
543 issue = Issue.generate!(:tracker => tracker, :status => status,
544 :project_id => 1, :author => user,
545 :assigned_to => group)
546 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
547 end
548
549 def test_new_statuses_allowed_to_should_consider_group_assignment
550 WorkflowTransition.delete_all
551 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
552 :old_status_id => 1, :new_status_id => 4,
553 :author => false, :assignee => true)
554 user = User.find(2)
555 group = Group.generate!
556 group.users << user
557
558 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
559 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
540 end
560 end
541
561
542 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
562 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
543 admin = User.find(1)
563 admin = User.find(1)
544 issue = Issue.find(1)
564 issue = Issue.find(1)
545 assert !admin.member_of?(issue.project)
565 assert !admin.member_of?(issue.project)
546 expected_statuses = [issue.status] +
566 expected_statuses = [issue.status] +
547 WorkflowTransition.find_all_by_old_status_id(
567 WorkflowTransition.find_all_by_old_status_id(
548 issue.status_id).map(&:new_status).uniq.sort
568 issue.status_id).map(&:new_status).uniq.sort
549 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
569 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
550 end
570 end
551
571
552 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
572 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
553 issue = Issue.find(1).copy
573 issue = Issue.find(1).copy
554 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
574 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
555
575
556 issue = Issue.find(2).copy
576 issue = Issue.find(2).copy
557 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
577 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
558 end
578 end
559
579
560 def test_safe_attributes_names_should_not_include_disabled_field
580 def test_safe_attributes_names_should_not_include_disabled_field
561 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
581 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
562
582
563 issue = Issue.new(:tracker => tracker)
583 issue = Issue.new(:tracker => tracker)
564 assert_include 'tracker_id', issue.safe_attribute_names
584 assert_include 'tracker_id', issue.safe_attribute_names
565 assert_include 'status_id', issue.safe_attribute_names
585 assert_include 'status_id', issue.safe_attribute_names
566 assert_include 'subject', issue.safe_attribute_names
586 assert_include 'subject', issue.safe_attribute_names
567 assert_include 'description', issue.safe_attribute_names
587 assert_include 'description', issue.safe_attribute_names
568 assert_include 'custom_field_values', issue.safe_attribute_names
588 assert_include 'custom_field_values', issue.safe_attribute_names
569 assert_include 'custom_fields', issue.safe_attribute_names
589 assert_include 'custom_fields', issue.safe_attribute_names
570 assert_include 'lock_version', issue.safe_attribute_names
590 assert_include 'lock_version', issue.safe_attribute_names
571
591
572 tracker.core_fields.each do |field|
592 tracker.core_fields.each do |field|
573 assert_include field, issue.safe_attribute_names
593 assert_include field, issue.safe_attribute_names
574 end
594 end
575
595
576 tracker.disabled_core_fields.each do |field|
596 tracker.disabled_core_fields.each do |field|
577 assert_not_include field, issue.safe_attribute_names
597 assert_not_include field, issue.safe_attribute_names
578 end
598 end
579 end
599 end
580
600
581 def test_safe_attributes_should_ignore_disabled_fields
601 def test_safe_attributes_should_ignore_disabled_fields
582 tracker = Tracker.find(1)
602 tracker = Tracker.find(1)
583 tracker.core_fields = %w(assigned_to_id due_date)
603 tracker.core_fields = %w(assigned_to_id due_date)
584 tracker.save!
604 tracker.save!
585
605
586 issue = Issue.new(:tracker => tracker)
606 issue = Issue.new(:tracker => tracker)
587 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
607 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
588 assert_nil issue.start_date
608 assert_nil issue.start_date
589 assert_equal Date.parse('2012-07-14'), issue.due_date
609 assert_equal Date.parse('2012-07-14'), issue.due_date
590 end
610 end
591
611
592 def test_safe_attributes_should_accept_target_tracker_enabled_fields
612 def test_safe_attributes_should_accept_target_tracker_enabled_fields
593 source = Tracker.find(1)
613 source = Tracker.find(1)
594 source.core_fields = []
614 source.core_fields = []
595 source.save!
615 source.save!
596 target = Tracker.find(2)
616 target = Tracker.find(2)
597 target.core_fields = %w(assigned_to_id due_date)
617 target.core_fields = %w(assigned_to_id due_date)
598 target.save!
618 target.save!
599
619
600 issue = Issue.new(:tracker => source)
620 issue = Issue.new(:tracker => source)
601 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
621 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
602 assert_equal target, issue.tracker
622 assert_equal target, issue.tracker
603 assert_equal Date.parse('2012-07-14'), issue.due_date
623 assert_equal Date.parse('2012-07-14'), issue.due_date
604 end
624 end
605
625
606 def test_safe_attributes_should_not_include_readonly_fields
626 def test_safe_attributes_should_not_include_readonly_fields
607 WorkflowPermission.delete_all
627 WorkflowPermission.delete_all
608 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
628 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
609 :role_id => 1, :field_name => 'due_date',
629 :role_id => 1, :field_name => 'due_date',
610 :rule => 'readonly')
630 :rule => 'readonly')
611 user = User.find(2)
631 user = User.find(2)
612
632
613 issue = Issue.new(:project_id => 1, :tracker_id => 1)
633 issue = Issue.new(:project_id => 1, :tracker_id => 1)
614 assert_equal %w(due_date), issue.read_only_attribute_names(user)
634 assert_equal %w(due_date), issue.read_only_attribute_names(user)
615 assert_not_include 'due_date', issue.safe_attribute_names(user)
635 assert_not_include 'due_date', issue.safe_attribute_names(user)
616
636
617 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
637 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
618 assert_equal Date.parse('2012-07-14'), issue.start_date
638 assert_equal Date.parse('2012-07-14'), issue.start_date
619 assert_nil issue.due_date
639 assert_nil issue.due_date
620 end
640 end
621
641
622 def test_safe_attributes_should_not_include_readonly_custom_fields
642 def test_safe_attributes_should_not_include_readonly_custom_fields
623 cf1 = IssueCustomField.create!(:name => 'Writable field',
643 cf1 = IssueCustomField.create!(:name => 'Writable field',
624 :field_format => 'string',
644 :field_format => 'string',
625 :is_for_all => true, :tracker_ids => [1])
645 :is_for_all => true, :tracker_ids => [1])
626 cf2 = IssueCustomField.create!(:name => 'Readonly field',
646 cf2 = IssueCustomField.create!(:name => 'Readonly field',
627 :field_format => 'string',
647 :field_format => 'string',
628 :is_for_all => true, :tracker_ids => [1])
648 :is_for_all => true, :tracker_ids => [1])
629 WorkflowPermission.delete_all
649 WorkflowPermission.delete_all
630 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
650 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
631 :role_id => 1, :field_name => cf2.id.to_s,
651 :role_id => 1, :field_name => cf2.id.to_s,
632 :rule => 'readonly')
652 :rule => 'readonly')
633 user = User.find(2)
653 user = User.find(2)
634 issue = Issue.new(:project_id => 1, :tracker_id => 1)
654 issue = Issue.new(:project_id => 1, :tracker_id => 1)
635 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
655 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
636 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
656 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
637
657
638 issue.send :safe_attributes=, {'custom_field_values' => {
658 issue.send :safe_attributes=, {'custom_field_values' => {
639 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
659 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
640 }}, user
660 }}, user
641 assert_equal 'value1', issue.custom_field_value(cf1)
661 assert_equal 'value1', issue.custom_field_value(cf1)
642 assert_nil issue.custom_field_value(cf2)
662 assert_nil issue.custom_field_value(cf2)
643
663
644 issue.send :safe_attributes=, {'custom_fields' => [
664 issue.send :safe_attributes=, {'custom_fields' => [
645 {'id' => cf1.id.to_s, 'value' => 'valuea'},
665 {'id' => cf1.id.to_s, 'value' => 'valuea'},
646 {'id' => cf2.id.to_s, 'value' => 'valueb'}
666 {'id' => cf2.id.to_s, 'value' => 'valueb'}
647 ]}, user
667 ]}, user
648 assert_equal 'valuea', issue.custom_field_value(cf1)
668 assert_equal 'valuea', issue.custom_field_value(cf1)
649 assert_nil issue.custom_field_value(cf2)
669 assert_nil issue.custom_field_value(cf2)
650 end
670 end
651
671
652 def test_editable_custom_field_values_should_return_non_readonly_custom_values
672 def test_editable_custom_field_values_should_return_non_readonly_custom_values
653 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
673 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
654 :is_for_all => true, :tracker_ids => [1, 2])
674 :is_for_all => true, :tracker_ids => [1, 2])
655 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
675 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
656 :is_for_all => true, :tracker_ids => [1, 2])
676 :is_for_all => true, :tracker_ids => [1, 2])
657 WorkflowPermission.delete_all
677 WorkflowPermission.delete_all
658 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
678 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
659 :field_name => cf2.id.to_s, :rule => 'readonly')
679 :field_name => cf2.id.to_s, :rule => 'readonly')
660 user = User.find(2)
680 user = User.find(2)
661
681
662 issue = Issue.new(:project_id => 1, :tracker_id => 1)
682 issue = Issue.new(:project_id => 1, :tracker_id => 1)
663 values = issue.editable_custom_field_values(user)
683 values = issue.editable_custom_field_values(user)
664 assert values.detect {|value| value.custom_field == cf1}
684 assert values.detect {|value| value.custom_field == cf1}
665 assert_nil values.detect {|value| value.custom_field == cf2}
685 assert_nil values.detect {|value| value.custom_field == cf2}
666
686
667 issue.tracker_id = 2
687 issue.tracker_id = 2
668 values = issue.editable_custom_field_values(user)
688 values = issue.editable_custom_field_values(user)
669 assert values.detect {|value| value.custom_field == cf1}
689 assert values.detect {|value| value.custom_field == cf1}
670 assert values.detect {|value| value.custom_field == cf2}
690 assert values.detect {|value| value.custom_field == cf2}
671 end
691 end
672
692
673 def test_safe_attributes_should_accept_target_tracker_writable_fields
693 def test_safe_attributes_should_accept_target_tracker_writable_fields
674 WorkflowPermission.delete_all
694 WorkflowPermission.delete_all
675 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
695 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
676 :role_id => 1, :field_name => 'due_date',
696 :role_id => 1, :field_name => 'due_date',
677 :rule => 'readonly')
697 :rule => 'readonly')
678 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
698 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
679 :role_id => 1, :field_name => 'start_date',
699 :role_id => 1, :field_name => 'start_date',
680 :rule => 'readonly')
700 :rule => 'readonly')
681 user = User.find(2)
701 user = User.find(2)
682
702
683 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
703 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
684
704
685 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
705 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
686 'due_date' => '2012-07-14'}, user
706 'due_date' => '2012-07-14'}, user
687 assert_equal Date.parse('2012-07-12'), issue.start_date
707 assert_equal Date.parse('2012-07-12'), issue.start_date
688 assert_nil issue.due_date
708 assert_nil issue.due_date
689
709
690 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
710 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
691 'due_date' => '2012-07-16',
711 'due_date' => '2012-07-16',
692 'tracker_id' => 2}, user
712 'tracker_id' => 2}, user
693 assert_equal Date.parse('2012-07-12'), issue.start_date
713 assert_equal Date.parse('2012-07-12'), issue.start_date
694 assert_equal Date.parse('2012-07-16'), issue.due_date
714 assert_equal Date.parse('2012-07-16'), issue.due_date
695 end
715 end
696
716
697 def test_safe_attributes_should_accept_target_status_writable_fields
717 def test_safe_attributes_should_accept_target_status_writable_fields
698 WorkflowPermission.delete_all
718 WorkflowPermission.delete_all
699 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
719 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
700 :role_id => 1, :field_name => 'due_date',
720 :role_id => 1, :field_name => 'due_date',
701 :rule => 'readonly')
721 :rule => 'readonly')
702 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
722 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
703 :role_id => 1, :field_name => 'start_date',
723 :role_id => 1, :field_name => 'start_date',
704 :rule => 'readonly')
724 :rule => 'readonly')
705 user = User.find(2)
725 user = User.find(2)
706
726
707 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
727 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
708
728
709 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
729 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
710 'due_date' => '2012-07-14'},
730 'due_date' => '2012-07-14'},
711 user
731 user
712 assert_equal Date.parse('2012-07-12'), issue.start_date
732 assert_equal Date.parse('2012-07-12'), issue.start_date
713 assert_nil issue.due_date
733 assert_nil issue.due_date
714
734
715 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
735 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
716 'due_date' => '2012-07-16',
736 'due_date' => '2012-07-16',
717 'status_id' => 2},
737 'status_id' => 2},
718 user
738 user
719 assert_equal Date.parse('2012-07-12'), issue.start_date
739 assert_equal Date.parse('2012-07-12'), issue.start_date
720 assert_equal Date.parse('2012-07-16'), issue.due_date
740 assert_equal Date.parse('2012-07-16'), issue.due_date
721 end
741 end
722
742
723 def test_required_attributes_should_be_validated
743 def test_required_attributes_should_be_validated
724 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
744 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
725 :is_for_all => true, :tracker_ids => [1, 2])
745 :is_for_all => true, :tracker_ids => [1, 2])
726
746
727 WorkflowPermission.delete_all
747 WorkflowPermission.delete_all
728 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
748 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
729 :role_id => 1, :field_name => 'due_date',
749 :role_id => 1, :field_name => 'due_date',
730 :rule => 'required')
750 :rule => 'required')
731 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
751 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
732 :role_id => 1, :field_name => 'category_id',
752 :role_id => 1, :field_name => 'category_id',
733 :rule => 'required')
753 :rule => 'required')
734 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
754 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
735 :role_id => 1, :field_name => cf.id.to_s,
755 :role_id => 1, :field_name => cf.id.to_s,
736 :rule => 'required')
756 :rule => 'required')
737
757
738 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
758 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
739 :role_id => 1, :field_name => 'start_date',
759 :role_id => 1, :field_name => 'start_date',
740 :rule => 'required')
760 :rule => 'required')
741 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
761 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
742 :role_id => 1, :field_name => cf.id.to_s,
762 :role_id => 1, :field_name => cf.id.to_s,
743 :rule => 'required')
763 :rule => 'required')
744 user = User.find(2)
764 user = User.find(2)
745
765
746 issue = Issue.new(:project_id => 1, :tracker_id => 1,
766 issue = Issue.new(:project_id => 1, :tracker_id => 1,
747 :status_id => 1, :subject => 'Required fields',
767 :status_id => 1, :subject => 'Required fields',
748 :author => user)
768 :author => user)
749 assert_equal [cf.id.to_s, "category_id", "due_date"],
769 assert_equal [cf.id.to_s, "category_id", "due_date"],
750 issue.required_attribute_names(user).sort
770 issue.required_attribute_names(user).sort
751 assert !issue.save, "Issue was saved"
771 assert !issue.save, "Issue was saved"
752 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
772 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
753 issue.errors.full_messages.sort
773 issue.errors.full_messages.sort
754
774
755 issue.tracker_id = 2
775 issue.tracker_id = 2
756 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
776 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
757 assert !issue.save, "Issue was saved"
777 assert !issue.save, "Issue was saved"
758 assert_equal ["Foo can't be blank", "Start date can't be blank"],
778 assert_equal ["Foo can't be blank", "Start date can't be blank"],
759 issue.errors.full_messages.sort
779 issue.errors.full_messages.sort
760
780
761 issue.start_date = Date.today
781 issue.start_date = Date.today
762 issue.custom_field_values = {cf.id.to_s => 'bar'}
782 issue.custom_field_values = {cf.id.to_s => 'bar'}
763 assert issue.save
783 assert issue.save
764 end
784 end
765
785
766 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
786 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
767 WorkflowPermission.delete_all
787 WorkflowPermission.delete_all
768 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
788 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
769 :role_id => 1, :field_name => 'due_date',
789 :role_id => 1, :field_name => 'due_date',
770 :rule => 'required')
790 :rule => 'required')
771 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
791 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
772 :role_id => 1, :field_name => 'start_date',
792 :role_id => 1, :field_name => 'start_date',
773 :rule => 'required')
793 :rule => 'required')
774 user = User.find(2)
794 user = User.find(2)
775 member = Member.find(1)
795 member = Member.find(1)
776 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
796 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
777
797
778 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
798 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
779
799
780 member.role_ids = [1, 2]
800 member.role_ids = [1, 2]
781 member.save!
801 member.save!
782 assert_equal [], issue.required_attribute_names(user.reload)
802 assert_equal [], issue.required_attribute_names(user.reload)
783
803
784 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
804 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
785 :role_id => 2, :field_name => 'due_date',
805 :role_id => 2, :field_name => 'due_date',
786 :rule => 'required')
806 :rule => 'required')
787 assert_equal %w(due_date), issue.required_attribute_names(user)
807 assert_equal %w(due_date), issue.required_attribute_names(user)
788
808
789 member.role_ids = [1, 2, 3]
809 member.role_ids = [1, 2, 3]
790 member.save!
810 member.save!
791 assert_equal [], issue.required_attribute_names(user.reload)
811 assert_equal [], issue.required_attribute_names(user.reload)
792
812
793 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
813 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
794 :role_id => 2, :field_name => 'due_date',
814 :role_id => 2, :field_name => 'due_date',
795 :rule => 'readonly')
815 :rule => 'readonly')
796 # required + readonly => required
816 # required + readonly => required
797 assert_equal %w(due_date), issue.required_attribute_names(user)
817 assert_equal %w(due_date), issue.required_attribute_names(user)
798 end
818 end
799
819
800 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
820 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
801 WorkflowPermission.delete_all
821 WorkflowPermission.delete_all
802 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
822 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
803 :role_id => 1, :field_name => 'due_date',
823 :role_id => 1, :field_name => 'due_date',
804 :rule => 'readonly')
824 :rule => 'readonly')
805 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
825 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
806 :role_id => 1, :field_name => 'start_date',
826 :role_id => 1, :field_name => 'start_date',
807 :rule => 'readonly')
827 :rule => 'readonly')
808 user = User.find(2)
828 user = User.find(2)
809 member = Member.find(1)
829 member = Member.find(1)
810 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
830 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
811
831
812 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
832 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
813
833
814 member.role_ids = [1, 2]
834 member.role_ids = [1, 2]
815 member.save!
835 member.save!
816 assert_equal [], issue.read_only_attribute_names(user.reload)
836 assert_equal [], issue.read_only_attribute_names(user.reload)
817
837
818 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
838 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
819 :role_id => 2, :field_name => 'due_date',
839 :role_id => 2, :field_name => 'due_date',
820 :rule => 'readonly')
840 :rule => 'readonly')
821 assert_equal %w(due_date), issue.read_only_attribute_names(user)
841 assert_equal %w(due_date), issue.read_only_attribute_names(user)
822 end
842 end
823
843
824 def test_copy
844 def test_copy
825 issue = Issue.new.copy_from(1)
845 issue = Issue.new.copy_from(1)
826 assert issue.copy?
846 assert issue.copy?
827 assert issue.save
847 assert issue.save
828 issue.reload
848 issue.reload
829 orig = Issue.find(1)
849 orig = Issue.find(1)
830 assert_equal orig.subject, issue.subject
850 assert_equal orig.subject, issue.subject
831 assert_equal orig.tracker, issue.tracker
851 assert_equal orig.tracker, issue.tracker
832 assert_equal "125", issue.custom_value_for(2).value
852 assert_equal "125", issue.custom_value_for(2).value
833 end
853 end
834
854
835 def test_copy_should_copy_status
855 def test_copy_should_copy_status
836 orig = Issue.find(8)
856 orig = Issue.find(8)
837 assert orig.status != IssueStatus.default
857 assert orig.status != IssueStatus.default
838
858
839 issue = Issue.new.copy_from(orig)
859 issue = Issue.new.copy_from(orig)
840 assert issue.save
860 assert issue.save
841 issue.reload
861 issue.reload
842 assert_equal orig.status, issue.status
862 assert_equal orig.status, issue.status
843 end
863 end
844
864
845 def test_copy_should_add_relation_with_copied_issue
865 def test_copy_should_add_relation_with_copied_issue
846 copied = Issue.find(1)
866 copied = Issue.find(1)
847 issue = Issue.new.copy_from(copied)
867 issue = Issue.new.copy_from(copied)
848 assert issue.save
868 assert issue.save
849 issue.reload
869 issue.reload
850
870
851 assert_equal 1, issue.relations.size
871 assert_equal 1, issue.relations.size
852 relation = issue.relations.first
872 relation = issue.relations.first
853 assert_equal 'copied_to', relation.relation_type
873 assert_equal 'copied_to', relation.relation_type
854 assert_equal copied, relation.issue_from
874 assert_equal copied, relation.issue_from
855 assert_equal issue, relation.issue_to
875 assert_equal issue, relation.issue_to
856 end
876 end
857
877
858 def test_copy_should_copy_subtasks
878 def test_copy_should_copy_subtasks
859 issue = Issue.generate_with_descendants!
879 issue = Issue.generate_with_descendants!
860
880
861 copy = issue.reload.copy
881 copy = issue.reload.copy
862 copy.author = User.find(7)
882 copy.author = User.find(7)
863 assert_difference 'Issue.count', 1+issue.descendants.count do
883 assert_difference 'Issue.count', 1+issue.descendants.count do
864 assert copy.save
884 assert copy.save
865 end
885 end
866 copy.reload
886 copy.reload
867 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
887 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
868 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
888 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
869 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
889 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
870 assert_equal copy.author, child_copy.author
890 assert_equal copy.author, child_copy.author
871 end
891 end
872
892
873 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
893 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
874 parent = Issue.generate!
894 parent = Issue.generate!
875 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
895 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
876 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
896 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
877
897
878 copy = parent.reload.copy
898 copy = parent.reload.copy
879 copy.parent_issue_id = parent.id
899 copy.parent_issue_id = parent.id
880 copy.author = User.find(7)
900 copy.author = User.find(7)
881 assert_difference 'Issue.count', 3 do
901 assert_difference 'Issue.count', 3 do
882 assert copy.save
902 assert copy.save
883 end
903 end
884 parent.reload
904 parent.reload
885 copy.reload
905 copy.reload
886 assert_equal parent, copy.parent
906 assert_equal parent, copy.parent
887 assert_equal 3, parent.children.count
907 assert_equal 3, parent.children.count
888 assert_equal 5, parent.descendants.count
908 assert_equal 5, parent.descendants.count
889 assert_equal 2, copy.children.count
909 assert_equal 2, copy.children.count
890 assert_equal 2, copy.descendants.count
910 assert_equal 2, copy.descendants.count
891 end
911 end
892
912
893 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
913 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
894 parent = Issue.generate!
914 parent = Issue.generate!
895 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
915 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
896 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
916 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
897
917
898 copy = parent.reload.copy
918 copy = parent.reload.copy
899 copy.parent_issue_id = child1.id
919 copy.parent_issue_id = child1.id
900 copy.author = User.find(7)
920 copy.author = User.find(7)
901 assert_difference 'Issue.count', 3 do
921 assert_difference 'Issue.count', 3 do
902 assert copy.save
922 assert copy.save
903 end
923 end
904 parent.reload
924 parent.reload
905 child1.reload
925 child1.reload
906 copy.reload
926 copy.reload
907 assert_equal child1, copy.parent
927 assert_equal child1, copy.parent
908 assert_equal 2, parent.children.count
928 assert_equal 2, parent.children.count
909 assert_equal 5, parent.descendants.count
929 assert_equal 5, parent.descendants.count
910 assert_equal 1, child1.children.count
930 assert_equal 1, child1.children.count
911 assert_equal 3, child1.descendants.count
931 assert_equal 3, child1.descendants.count
912 assert_equal 2, copy.children.count
932 assert_equal 2, copy.children.count
913 assert_equal 2, copy.descendants.count
933 assert_equal 2, copy.descendants.count
914 end
934 end
915
935
916 def test_copy_should_copy_subtasks_to_target_project
936 def test_copy_should_copy_subtasks_to_target_project
917 issue = Issue.generate_with_descendants!
937 issue = Issue.generate_with_descendants!
918
938
919 copy = issue.copy(:project_id => 3)
939 copy = issue.copy(:project_id => 3)
920 assert_difference 'Issue.count', 1+issue.descendants.count do
940 assert_difference 'Issue.count', 1+issue.descendants.count do
921 assert copy.save
941 assert copy.save
922 end
942 end
923 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
943 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
924 end
944 end
925
945
926 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
946 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
927 issue = Issue.generate_with_descendants!
947 issue = Issue.generate_with_descendants!
928
948
929 copy = issue.reload.copy
949 copy = issue.reload.copy
930 assert_difference 'Issue.count', 1+issue.descendants.count do
950 assert_difference 'Issue.count', 1+issue.descendants.count do
931 assert copy.save
951 assert copy.save
932 assert copy.save
952 assert copy.save
933 end
953 end
934 end
954 end
935
955
936 def test_should_not_call_after_project_change_on_creation
956 def test_should_not_call_after_project_change_on_creation
937 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
957 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
938 :subject => 'Test', :author_id => 1)
958 :subject => 'Test', :author_id => 1)
939 issue.expects(:after_project_change).never
959 issue.expects(:after_project_change).never
940 issue.save!
960 issue.save!
941 end
961 end
942
962
943 def test_should_not_call_after_project_change_on_update
963 def test_should_not_call_after_project_change_on_update
944 issue = Issue.find(1)
964 issue = Issue.find(1)
945 issue.project = Project.find(1)
965 issue.project = Project.find(1)
946 issue.subject = 'No project change'
966 issue.subject = 'No project change'
947 issue.expects(:after_project_change).never
967 issue.expects(:after_project_change).never
948 issue.save!
968 issue.save!
949 end
969 end
950
970
951 def test_should_call_after_project_change_on_project_change
971 def test_should_call_after_project_change_on_project_change
952 issue = Issue.find(1)
972 issue = Issue.find(1)
953 issue.project = Project.find(2)
973 issue.project = Project.find(2)
954 issue.expects(:after_project_change).once
974 issue.expects(:after_project_change).once
955 issue.save!
975 issue.save!
956 end
976 end
957
977
958 def test_adding_journal_should_update_timestamp
978 def test_adding_journal_should_update_timestamp
959 issue = Issue.find(1)
979 issue = Issue.find(1)
960 updated_on_was = issue.updated_on
980 updated_on_was = issue.updated_on
961
981
962 issue.init_journal(User.first, "Adding notes")
982 issue.init_journal(User.first, "Adding notes")
963 assert_difference 'Journal.count' do
983 assert_difference 'Journal.count' do
964 assert issue.save
984 assert issue.save
965 end
985 end
966 issue.reload
986 issue.reload
967
987
968 assert_not_equal updated_on_was, issue.updated_on
988 assert_not_equal updated_on_was, issue.updated_on
969 end
989 end
970
990
971 def test_should_close_duplicates
991 def test_should_close_duplicates
972 # Create 3 issues
992 # Create 3 issues
973 issue1 = Issue.generate!
993 issue1 = Issue.generate!
974 issue2 = Issue.generate!
994 issue2 = Issue.generate!
975 issue3 = Issue.generate!
995 issue3 = Issue.generate!
976
996
977 # 2 is a dupe of 1
997 # 2 is a dupe of 1
978 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
998 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
979 :relation_type => IssueRelation::TYPE_DUPLICATES)
999 :relation_type => IssueRelation::TYPE_DUPLICATES)
980 # And 3 is a dupe of 2
1000 # And 3 is a dupe of 2
981 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1001 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
982 :relation_type => IssueRelation::TYPE_DUPLICATES)
1002 :relation_type => IssueRelation::TYPE_DUPLICATES)
983 # And 3 is a dupe of 1 (circular duplicates)
1003 # And 3 is a dupe of 1 (circular duplicates)
984 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1004 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
985 :relation_type => IssueRelation::TYPE_DUPLICATES)
1005 :relation_type => IssueRelation::TYPE_DUPLICATES)
986
1006
987 assert issue1.reload.duplicates.include?(issue2)
1007 assert issue1.reload.duplicates.include?(issue2)
988
1008
989 # Closing issue 1
1009 # Closing issue 1
990 issue1.init_journal(User.first, "Closing issue1")
1010 issue1.init_journal(User.first, "Closing issue1")
991 issue1.status = IssueStatus.where(:is_closed => true).first
1011 issue1.status = IssueStatus.where(:is_closed => true).first
992 assert issue1.save
1012 assert issue1.save
993 # 2 and 3 should be also closed
1013 # 2 and 3 should be also closed
994 assert issue2.reload.closed?
1014 assert issue2.reload.closed?
995 assert issue3.reload.closed?
1015 assert issue3.reload.closed?
996 end
1016 end
997
1017
998 def test_should_not_close_duplicated_issue
1018 def test_should_not_close_duplicated_issue
999 issue1 = Issue.generate!
1019 issue1 = Issue.generate!
1000 issue2 = Issue.generate!
1020 issue2 = Issue.generate!
1001
1021
1002 # 2 is a dupe of 1
1022 # 2 is a dupe of 1
1003 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1023 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1004 :relation_type => IssueRelation::TYPE_DUPLICATES)
1024 :relation_type => IssueRelation::TYPE_DUPLICATES)
1005 # 2 is a dup of 1 but 1 is not a duplicate of 2
1025 # 2 is a dup of 1 but 1 is not a duplicate of 2
1006 assert !issue2.reload.duplicates.include?(issue1)
1026 assert !issue2.reload.duplicates.include?(issue1)
1007
1027
1008 # Closing issue 2
1028 # Closing issue 2
1009 issue2.init_journal(User.first, "Closing issue2")
1029 issue2.init_journal(User.first, "Closing issue2")
1010 issue2.status = IssueStatus.where(:is_closed => true).first
1030 issue2.status = IssueStatus.where(:is_closed => true).first
1011 assert issue2.save
1031 assert issue2.save
1012 # 1 should not be also closed
1032 # 1 should not be also closed
1013 assert !issue1.reload.closed?
1033 assert !issue1.reload.closed?
1014 end
1034 end
1015
1035
1016 def test_assignable_versions
1036 def test_assignable_versions
1017 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1037 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1018 :status_id => 1, :fixed_version_id => 1,
1038 :status_id => 1, :fixed_version_id => 1,
1019 :subject => 'New issue')
1039 :subject => 'New issue')
1020 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1040 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1021 end
1041 end
1022
1042
1023 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1043 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1024 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1044 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1025 :status_id => 1, :fixed_version_id => 1,
1045 :status_id => 1, :fixed_version_id => 1,
1026 :subject => 'New issue')
1046 :subject => 'New issue')
1027 assert !issue.save
1047 assert !issue.save
1028 assert_not_nil issue.errors[:fixed_version_id]
1048 assert_not_nil issue.errors[:fixed_version_id]
1029 end
1049 end
1030
1050
1031 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1051 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1032 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1052 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1033 :status_id => 1, :fixed_version_id => 2,
1053 :status_id => 1, :fixed_version_id => 2,
1034 :subject => 'New issue')
1054 :subject => 'New issue')
1035 assert !issue.save
1055 assert !issue.save
1036 assert_not_nil issue.errors[:fixed_version_id]
1056 assert_not_nil issue.errors[:fixed_version_id]
1037 end
1057 end
1038
1058
1039 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1059 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1040 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1060 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1041 :status_id => 1, :fixed_version_id => 3,
1061 :status_id => 1, :fixed_version_id => 3,
1042 :subject => 'New issue')
1062 :subject => 'New issue')
1043 assert issue.save
1063 assert issue.save
1044 end
1064 end
1045
1065
1046 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1066 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1047 issue = Issue.find(11)
1067 issue = Issue.find(11)
1048 assert_equal 'closed', issue.fixed_version.status
1068 assert_equal 'closed', issue.fixed_version.status
1049 issue.subject = 'Subject changed'
1069 issue.subject = 'Subject changed'
1050 assert issue.save
1070 assert issue.save
1051 end
1071 end
1052
1072
1053 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1073 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1054 issue = Issue.find(11)
1074 issue = Issue.find(11)
1055 issue.status_id = 1
1075 issue.status_id = 1
1056 assert !issue.save
1076 assert !issue.save
1057 assert_not_nil issue.errors[:base]
1077 assert_not_nil issue.errors[:base]
1058 end
1078 end
1059
1079
1060 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1080 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1061 issue = Issue.find(11)
1081 issue = Issue.find(11)
1062 issue.status_id = 1
1082 issue.status_id = 1
1063 issue.fixed_version_id = 3
1083 issue.fixed_version_id = 3
1064 assert issue.save
1084 assert issue.save
1065 end
1085 end
1066
1086
1067 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1087 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1068 issue = Issue.find(12)
1088 issue = Issue.find(12)
1069 assert_equal 'locked', issue.fixed_version.status
1089 assert_equal 'locked', issue.fixed_version.status
1070 issue.status_id = 1
1090 issue.status_id = 1
1071 assert issue.save
1091 assert issue.save
1072 end
1092 end
1073
1093
1074 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1094 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1075 issue = Issue.find(2)
1095 issue = Issue.find(2)
1076 assert_equal 2, issue.fixed_version_id
1096 assert_equal 2, issue.fixed_version_id
1077 issue.project_id = 3
1097 issue.project_id = 3
1078 assert_nil issue.fixed_version_id
1098 assert_nil issue.fixed_version_id
1079 issue.fixed_version_id = 2
1099 issue.fixed_version_id = 2
1080 assert !issue.save
1100 assert !issue.save
1081 assert_include 'Target version is not included in the list', issue.errors.full_messages
1101 assert_include 'Target version is not included in the list', issue.errors.full_messages
1082 end
1102 end
1083
1103
1084 def test_should_keep_shared_version_when_changing_project
1104 def test_should_keep_shared_version_when_changing_project
1085 Version.find(2).update_attribute :sharing, 'tree'
1105 Version.find(2).update_attribute :sharing, 'tree'
1086
1106
1087 issue = Issue.find(2)
1107 issue = Issue.find(2)
1088 assert_equal 2, issue.fixed_version_id
1108 assert_equal 2, issue.fixed_version_id
1089 issue.project_id = 3
1109 issue.project_id = 3
1090 assert_equal 2, issue.fixed_version_id
1110 assert_equal 2, issue.fixed_version_id
1091 assert issue.save
1111 assert issue.save
1092 end
1112 end
1093
1113
1094 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1114 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1095 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1115 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1096 end
1116 end
1097
1117
1098 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1118 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1099 Project.find(2).disable_module! :issue_tracking
1119 Project.find(2).disable_module! :issue_tracking
1100 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1120 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1101 end
1121 end
1102
1122
1103 def test_move_to_another_project_with_same_category
1123 def test_move_to_another_project_with_same_category
1104 issue = Issue.find(1)
1124 issue = Issue.find(1)
1105 issue.project = Project.find(2)
1125 issue.project = Project.find(2)
1106 assert issue.save
1126 assert issue.save
1107 issue.reload
1127 issue.reload
1108 assert_equal 2, issue.project_id
1128 assert_equal 2, issue.project_id
1109 # Category changes
1129 # Category changes
1110 assert_equal 4, issue.category_id
1130 assert_equal 4, issue.category_id
1111 # Make sure time entries were move to the target project
1131 # Make sure time entries were move to the target project
1112 assert_equal 2, issue.time_entries.first.project_id
1132 assert_equal 2, issue.time_entries.first.project_id
1113 end
1133 end
1114
1134
1115 def test_move_to_another_project_without_same_category
1135 def test_move_to_another_project_without_same_category
1116 issue = Issue.find(2)
1136 issue = Issue.find(2)
1117 issue.project = Project.find(2)
1137 issue.project = Project.find(2)
1118 assert issue.save
1138 assert issue.save
1119 issue.reload
1139 issue.reload
1120 assert_equal 2, issue.project_id
1140 assert_equal 2, issue.project_id
1121 # Category cleared
1141 # Category cleared
1122 assert_nil issue.category_id
1142 assert_nil issue.category_id
1123 end
1143 end
1124
1144
1125 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1145 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1126 issue = Issue.find(1)
1146 issue = Issue.find(1)
1127 issue.update_attribute(:fixed_version_id, 1)
1147 issue.update_attribute(:fixed_version_id, 1)
1128 issue.project = Project.find(2)
1148 issue.project = Project.find(2)
1129 assert issue.save
1149 assert issue.save
1130 issue.reload
1150 issue.reload
1131 assert_equal 2, issue.project_id
1151 assert_equal 2, issue.project_id
1132 # Cleared fixed_version
1152 # Cleared fixed_version
1133 assert_equal nil, issue.fixed_version
1153 assert_equal nil, issue.fixed_version
1134 end
1154 end
1135
1155
1136 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1156 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1137 issue = Issue.find(1)
1157 issue = Issue.find(1)
1138 issue.update_attribute(:fixed_version_id, 4)
1158 issue.update_attribute(:fixed_version_id, 4)
1139 issue.project = Project.find(5)
1159 issue.project = Project.find(5)
1140 assert issue.save
1160 assert issue.save
1141 issue.reload
1161 issue.reload
1142 assert_equal 5, issue.project_id
1162 assert_equal 5, issue.project_id
1143 # Keep fixed_version
1163 # Keep fixed_version
1144 assert_equal 4, issue.fixed_version_id
1164 assert_equal 4, issue.fixed_version_id
1145 end
1165 end
1146
1166
1147 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1167 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1148 issue = Issue.find(1)
1168 issue = Issue.find(1)
1149 issue.update_attribute(:fixed_version_id, 1)
1169 issue.update_attribute(:fixed_version_id, 1)
1150 issue.project = Project.find(5)
1170 issue.project = Project.find(5)
1151 assert issue.save
1171 assert issue.save
1152 issue.reload
1172 issue.reload
1153 assert_equal 5, issue.project_id
1173 assert_equal 5, issue.project_id
1154 # Cleared fixed_version
1174 # Cleared fixed_version
1155 assert_equal nil, issue.fixed_version
1175 assert_equal nil, issue.fixed_version
1156 end
1176 end
1157
1177
1158 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1178 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1159 issue = Issue.find(1)
1179 issue = Issue.find(1)
1160 issue.update_attribute(:fixed_version_id, 7)
1180 issue.update_attribute(:fixed_version_id, 7)
1161 issue.project = Project.find(2)
1181 issue.project = Project.find(2)
1162 assert issue.save
1182 assert issue.save
1163 issue.reload
1183 issue.reload
1164 assert_equal 2, issue.project_id
1184 assert_equal 2, issue.project_id
1165 # Keep fixed_version
1185 # Keep fixed_version
1166 assert_equal 7, issue.fixed_version_id
1186 assert_equal 7, issue.fixed_version_id
1167 end
1187 end
1168
1188
1169 def test_move_to_another_project_should_keep_parent_if_valid
1189 def test_move_to_another_project_should_keep_parent_if_valid
1170 issue = Issue.find(1)
1190 issue = Issue.find(1)
1171 issue.update_attribute(:parent_issue_id, 2)
1191 issue.update_attribute(:parent_issue_id, 2)
1172 issue.project = Project.find(3)
1192 issue.project = Project.find(3)
1173 assert issue.save
1193 assert issue.save
1174 issue.reload
1194 issue.reload
1175 assert_equal 2, issue.parent_id
1195 assert_equal 2, issue.parent_id
1176 end
1196 end
1177
1197
1178 def test_move_to_another_project_should_clear_parent_if_not_valid
1198 def test_move_to_another_project_should_clear_parent_if_not_valid
1179 issue = Issue.find(1)
1199 issue = Issue.find(1)
1180 issue.update_attribute(:parent_issue_id, 2)
1200 issue.update_attribute(:parent_issue_id, 2)
1181 issue.project = Project.find(2)
1201 issue.project = Project.find(2)
1182 assert issue.save
1202 assert issue.save
1183 issue.reload
1203 issue.reload
1184 assert_nil issue.parent_id
1204 assert_nil issue.parent_id
1185 end
1205 end
1186
1206
1187 def test_move_to_another_project_with_disabled_tracker
1207 def test_move_to_another_project_with_disabled_tracker
1188 issue = Issue.find(1)
1208 issue = Issue.find(1)
1189 target = Project.find(2)
1209 target = Project.find(2)
1190 target.tracker_ids = [3]
1210 target.tracker_ids = [3]
1191 target.save
1211 target.save
1192 issue.project = target
1212 issue.project = target
1193 assert issue.save
1213 assert issue.save
1194 issue.reload
1214 issue.reload
1195 assert_equal 2, issue.project_id
1215 assert_equal 2, issue.project_id
1196 assert_equal 3, issue.tracker_id
1216 assert_equal 3, issue.tracker_id
1197 end
1217 end
1198
1218
1199 def test_copy_to_the_same_project
1219 def test_copy_to_the_same_project
1200 issue = Issue.find(1)
1220 issue = Issue.find(1)
1201 copy = issue.copy
1221 copy = issue.copy
1202 assert_difference 'Issue.count' do
1222 assert_difference 'Issue.count' do
1203 copy.save!
1223 copy.save!
1204 end
1224 end
1205 assert_kind_of Issue, copy
1225 assert_kind_of Issue, copy
1206 assert_equal issue.project, copy.project
1226 assert_equal issue.project, copy.project
1207 assert_equal "125", copy.custom_value_for(2).value
1227 assert_equal "125", copy.custom_value_for(2).value
1208 end
1228 end
1209
1229
1210 def test_copy_to_another_project_and_tracker
1230 def test_copy_to_another_project_and_tracker
1211 issue = Issue.find(1)
1231 issue = Issue.find(1)
1212 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1232 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1213 assert_difference 'Issue.count' do
1233 assert_difference 'Issue.count' do
1214 copy.save!
1234 copy.save!
1215 end
1235 end
1216 copy.reload
1236 copy.reload
1217 assert_kind_of Issue, copy
1237 assert_kind_of Issue, copy
1218 assert_equal Project.find(3), copy.project
1238 assert_equal Project.find(3), copy.project
1219 assert_equal Tracker.find(2), copy.tracker
1239 assert_equal Tracker.find(2), copy.tracker
1220 # Custom field #2 is not associated with target tracker
1240 # Custom field #2 is not associated with target tracker
1221 assert_nil copy.custom_value_for(2)
1241 assert_nil copy.custom_value_for(2)
1222 end
1242 end
1223
1243
1224 test "#copy should not create a journal" do
1244 test "#copy should not create a journal" do
1225 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1245 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1226 copy.save!
1246 copy.save!
1227 assert_equal 0, copy.reload.journals.size
1247 assert_equal 0, copy.reload.journals.size
1228 end
1248 end
1229
1249
1230 test "#copy should allow assigned_to changes" do
1250 test "#copy should allow assigned_to changes" do
1231 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1251 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1232 assert_equal 3, copy.assigned_to_id
1252 assert_equal 3, copy.assigned_to_id
1233 end
1253 end
1234
1254
1235 test "#copy should allow status changes" do
1255 test "#copy should allow status changes" do
1236 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1256 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1237 assert_equal 2, copy.status_id
1257 assert_equal 2, copy.status_id
1238 end
1258 end
1239
1259
1240 test "#copy should allow start date changes" do
1260 test "#copy should allow start date changes" do
1241 date = Date.today
1261 date = Date.today
1242 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1262 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1243 assert_equal date, copy.start_date
1263 assert_equal date, copy.start_date
1244 end
1264 end
1245
1265
1246 test "#copy should allow due date changes" do
1266 test "#copy should allow due date changes" do
1247 date = Date.today
1267 date = Date.today
1248 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1268 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1249 assert_equal date, copy.due_date
1269 assert_equal date, copy.due_date
1250 end
1270 end
1251
1271
1252 test "#copy should set current user as author" do
1272 test "#copy should set current user as author" do
1253 User.current = User.find(9)
1273 User.current = User.find(9)
1254 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1274 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1255 assert_equal User.current, copy.author
1275 assert_equal User.current, copy.author
1256 end
1276 end
1257
1277
1258 test "#copy should create a journal with notes" do
1278 test "#copy should create a journal with notes" do
1259 date = Date.today
1279 date = Date.today
1260 notes = "Notes added when copying"
1280 notes = "Notes added when copying"
1261 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1281 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1262 copy.init_journal(User.current, notes)
1282 copy.init_journal(User.current, notes)
1263 copy.save!
1283 copy.save!
1264
1284
1265 assert_equal 1, copy.journals.size
1285 assert_equal 1, copy.journals.size
1266 journal = copy.journals.first
1286 journal = copy.journals.first
1267 assert_equal 0, journal.details.size
1287 assert_equal 0, journal.details.size
1268 assert_equal notes, journal.notes
1288 assert_equal notes, journal.notes
1269 end
1289 end
1270
1290
1271 def test_valid_parent_project
1291 def test_valid_parent_project
1272 issue = Issue.find(1)
1292 issue = Issue.find(1)
1273 issue_in_same_project = Issue.find(2)
1293 issue_in_same_project = Issue.find(2)
1274 issue_in_child_project = Issue.find(5)
1294 issue_in_child_project = Issue.find(5)
1275 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1295 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1276 issue_in_other_child_project = Issue.find(6)
1296 issue_in_other_child_project = Issue.find(6)
1277 issue_in_different_tree = Issue.find(4)
1297 issue_in_different_tree = Issue.find(4)
1278
1298
1279 with_settings :cross_project_subtasks => '' do
1299 with_settings :cross_project_subtasks => '' do
1280 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1300 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1281 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1301 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1282 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1302 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1283 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1303 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1284 end
1304 end
1285
1305
1286 with_settings :cross_project_subtasks => 'system' do
1306 with_settings :cross_project_subtasks => 'system' do
1287 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1307 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1288 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1308 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1289 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1309 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1290 end
1310 end
1291
1311
1292 with_settings :cross_project_subtasks => 'tree' do
1312 with_settings :cross_project_subtasks => 'tree' do
1293 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1313 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1294 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1314 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1295 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1315 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1296 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1316 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1297
1317
1298 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1318 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1299 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1319 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1300 end
1320 end
1301
1321
1302 with_settings :cross_project_subtasks => 'descendants' do
1322 with_settings :cross_project_subtasks => 'descendants' do
1303 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1323 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1304 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1324 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1305 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1325 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1306 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1326 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1307
1327
1308 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1328 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1309 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1329 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1310 end
1330 end
1311 end
1331 end
1312
1332
1313 def test_recipients_should_include_previous_assignee
1333 def test_recipients_should_include_previous_assignee
1314 user = User.find(3)
1334 user = User.find(3)
1315 user.members.update_all ["mail_notification = ?", false]
1335 user.members.update_all ["mail_notification = ?", false]
1316 user.update_attribute :mail_notification, 'only_assigned'
1336 user.update_attribute :mail_notification, 'only_assigned'
1317
1337
1318 issue = Issue.find(2)
1338 issue = Issue.find(2)
1319 issue.assigned_to = nil
1339 issue.assigned_to = nil
1320 assert_include user.mail, issue.recipients
1340 assert_include user.mail, issue.recipients
1321 issue.save!
1341 issue.save!
1322 assert !issue.recipients.include?(user.mail)
1342 assert !issue.recipients.include?(user.mail)
1323 end
1343 end
1324
1344
1325 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1345 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1326 issue = Issue.find(12)
1346 issue = Issue.find(12)
1327 assert issue.recipients.include?(issue.author.mail)
1347 assert issue.recipients.include?(issue.author.mail)
1328 # copy the issue to a private project
1348 # copy the issue to a private project
1329 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1349 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1330 # author is not a member of project anymore
1350 # author is not a member of project anymore
1331 assert !copy.recipients.include?(copy.author.mail)
1351 assert !copy.recipients.include?(copy.author.mail)
1332 end
1352 end
1333
1353
1334 def test_recipients_should_include_the_assigned_group_members
1354 def test_recipients_should_include_the_assigned_group_members
1335 group_member = User.generate!
1355 group_member = User.generate!
1336 group = Group.generate!
1356 group = Group.generate!
1337 group.users << group_member
1357 group.users << group_member
1338
1358
1339 issue = Issue.find(12)
1359 issue = Issue.find(12)
1340 issue.assigned_to = group
1360 issue.assigned_to = group
1341 assert issue.recipients.include?(group_member.mail)
1361 assert issue.recipients.include?(group_member.mail)
1342 end
1362 end
1343
1363
1344 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1364 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1345 user = User.find(3)
1365 user = User.find(3)
1346 issue = Issue.find(9)
1366 issue = Issue.find(9)
1347 Watcher.create!(:user => user, :watchable => issue)
1367 Watcher.create!(:user => user, :watchable => issue)
1348 assert issue.watched_by?(user)
1368 assert issue.watched_by?(user)
1349 assert !issue.watcher_recipients.include?(user.mail)
1369 assert !issue.watcher_recipients.include?(user.mail)
1350 end
1370 end
1351
1371
1352 def test_issue_destroy
1372 def test_issue_destroy
1353 Issue.find(1).destroy
1373 Issue.find(1).destroy
1354 assert_nil Issue.find_by_id(1)
1374 assert_nil Issue.find_by_id(1)
1355 assert_nil TimeEntry.find_by_issue_id(1)
1375 assert_nil TimeEntry.find_by_issue_id(1)
1356 end
1376 end
1357
1377
1358 def test_destroying_a_deleted_issue_should_not_raise_an_error
1378 def test_destroying_a_deleted_issue_should_not_raise_an_error
1359 issue = Issue.find(1)
1379 issue = Issue.find(1)
1360 Issue.find(1).destroy
1380 Issue.find(1).destroy
1361
1381
1362 assert_nothing_raised do
1382 assert_nothing_raised do
1363 assert_no_difference 'Issue.count' do
1383 assert_no_difference 'Issue.count' do
1364 issue.destroy
1384 issue.destroy
1365 end
1385 end
1366 assert issue.destroyed?
1386 assert issue.destroyed?
1367 end
1387 end
1368 end
1388 end
1369
1389
1370 def test_destroying_a_stale_issue_should_not_raise_an_error
1390 def test_destroying_a_stale_issue_should_not_raise_an_error
1371 issue = Issue.find(1)
1391 issue = Issue.find(1)
1372 Issue.find(1).update_attribute :subject, "Updated"
1392 Issue.find(1).update_attribute :subject, "Updated"
1373
1393
1374 assert_nothing_raised do
1394 assert_nothing_raised do
1375 assert_difference 'Issue.count', -1 do
1395 assert_difference 'Issue.count', -1 do
1376 issue.destroy
1396 issue.destroy
1377 end
1397 end
1378 assert issue.destroyed?
1398 assert issue.destroyed?
1379 end
1399 end
1380 end
1400 end
1381
1401
1382 def test_blocked
1402 def test_blocked
1383 blocked_issue = Issue.find(9)
1403 blocked_issue = Issue.find(9)
1384 blocking_issue = Issue.find(10)
1404 blocking_issue = Issue.find(10)
1385
1405
1386 assert blocked_issue.blocked?
1406 assert blocked_issue.blocked?
1387 assert !blocking_issue.blocked?
1407 assert !blocking_issue.blocked?
1388 end
1408 end
1389
1409
1390 def test_blocked_issues_dont_allow_closed_statuses
1410 def test_blocked_issues_dont_allow_closed_statuses
1391 blocked_issue = Issue.find(9)
1411 blocked_issue = Issue.find(9)
1392
1412
1393 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1413 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1394 assert !allowed_statuses.empty?
1414 assert !allowed_statuses.empty?
1395 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1415 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1396 assert closed_statuses.empty?
1416 assert closed_statuses.empty?
1397 end
1417 end
1398
1418
1399 def test_unblocked_issues_allow_closed_statuses
1419 def test_unblocked_issues_allow_closed_statuses
1400 blocking_issue = Issue.find(10)
1420 blocking_issue = Issue.find(10)
1401
1421
1402 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1422 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1403 assert !allowed_statuses.empty?
1423 assert !allowed_statuses.empty?
1404 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1424 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1405 assert !closed_statuses.empty?
1425 assert !closed_statuses.empty?
1406 end
1426 end
1407
1427
1408 def test_reschedule_an_issue_without_dates
1428 def test_reschedule_an_issue_without_dates
1409 with_settings :non_working_week_days => [] do
1429 with_settings :non_working_week_days => [] do
1410 issue = Issue.new(:start_date => nil, :due_date => nil)
1430 issue = Issue.new(:start_date => nil, :due_date => nil)
1411 issue.reschedule_on '2012-10-09'.to_date
1431 issue.reschedule_on '2012-10-09'.to_date
1412 assert_equal '2012-10-09'.to_date, issue.start_date
1432 assert_equal '2012-10-09'.to_date, issue.start_date
1413 assert_equal '2012-10-09'.to_date, issue.due_date
1433 assert_equal '2012-10-09'.to_date, issue.due_date
1414 end
1434 end
1415
1435
1416 with_settings :non_working_week_days => %w(6 7) do
1436 with_settings :non_working_week_days => %w(6 7) do
1417 issue = Issue.new(:start_date => nil, :due_date => nil)
1437 issue = Issue.new(:start_date => nil, :due_date => nil)
1418 issue.reschedule_on '2012-10-09'.to_date
1438 issue.reschedule_on '2012-10-09'.to_date
1419 assert_equal '2012-10-09'.to_date, issue.start_date
1439 assert_equal '2012-10-09'.to_date, issue.start_date
1420 assert_equal '2012-10-09'.to_date, issue.due_date
1440 assert_equal '2012-10-09'.to_date, issue.due_date
1421
1441
1422 issue = Issue.new(:start_date => nil, :due_date => nil)
1442 issue = Issue.new(:start_date => nil, :due_date => nil)
1423 issue.reschedule_on '2012-10-13'.to_date
1443 issue.reschedule_on '2012-10-13'.to_date
1424 assert_equal '2012-10-15'.to_date, issue.start_date
1444 assert_equal '2012-10-15'.to_date, issue.start_date
1425 assert_equal '2012-10-15'.to_date, issue.due_date
1445 assert_equal '2012-10-15'.to_date, issue.due_date
1426 end
1446 end
1427 end
1447 end
1428
1448
1429 def test_reschedule_an_issue_with_start_date
1449 def test_reschedule_an_issue_with_start_date
1430 with_settings :non_working_week_days => [] do
1450 with_settings :non_working_week_days => [] do
1431 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1451 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1432 issue.reschedule_on '2012-10-13'.to_date
1452 issue.reschedule_on '2012-10-13'.to_date
1433 assert_equal '2012-10-13'.to_date, issue.start_date
1453 assert_equal '2012-10-13'.to_date, issue.start_date
1434 assert_equal '2012-10-13'.to_date, issue.due_date
1454 assert_equal '2012-10-13'.to_date, issue.due_date
1435 end
1455 end
1436
1456
1437 with_settings :non_working_week_days => %w(6 7) do
1457 with_settings :non_working_week_days => %w(6 7) do
1438 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1458 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1439 issue.reschedule_on '2012-10-11'.to_date
1459 issue.reschedule_on '2012-10-11'.to_date
1440 assert_equal '2012-10-11'.to_date, issue.start_date
1460 assert_equal '2012-10-11'.to_date, issue.start_date
1441 assert_equal '2012-10-11'.to_date, issue.due_date
1461 assert_equal '2012-10-11'.to_date, issue.due_date
1442
1462
1443 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1463 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1444 issue.reschedule_on '2012-10-13'.to_date
1464 issue.reschedule_on '2012-10-13'.to_date
1445 assert_equal '2012-10-15'.to_date, issue.start_date
1465 assert_equal '2012-10-15'.to_date, issue.start_date
1446 assert_equal '2012-10-15'.to_date, issue.due_date
1466 assert_equal '2012-10-15'.to_date, issue.due_date
1447 end
1467 end
1448 end
1468 end
1449
1469
1450 def test_reschedule_an_issue_with_start_and_due_dates
1470 def test_reschedule_an_issue_with_start_and_due_dates
1451 with_settings :non_working_week_days => [] do
1471 with_settings :non_working_week_days => [] do
1452 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1472 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1453 issue.reschedule_on '2012-10-13'.to_date
1473 issue.reschedule_on '2012-10-13'.to_date
1454 assert_equal '2012-10-13'.to_date, issue.start_date
1474 assert_equal '2012-10-13'.to_date, issue.start_date
1455 assert_equal '2012-10-19'.to_date, issue.due_date
1475 assert_equal '2012-10-19'.to_date, issue.due_date
1456 end
1476 end
1457
1477
1458 with_settings :non_working_week_days => %w(6 7) do
1478 with_settings :non_working_week_days => %w(6 7) do
1459 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1479 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1460 issue.reschedule_on '2012-10-11'.to_date
1480 issue.reschedule_on '2012-10-11'.to_date
1461 assert_equal '2012-10-11'.to_date, issue.start_date
1481 assert_equal '2012-10-11'.to_date, issue.start_date
1462 assert_equal '2012-10-23'.to_date, issue.due_date
1482 assert_equal '2012-10-23'.to_date, issue.due_date
1463
1483
1464 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1484 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1465 issue.reschedule_on '2012-10-13'.to_date
1485 issue.reschedule_on '2012-10-13'.to_date
1466 assert_equal '2012-10-15'.to_date, issue.start_date
1486 assert_equal '2012-10-15'.to_date, issue.start_date
1467 assert_equal '2012-10-25'.to_date, issue.due_date
1487 assert_equal '2012-10-25'.to_date, issue.due_date
1468 end
1488 end
1469 end
1489 end
1470
1490
1471 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1491 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1472 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1492 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1473 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1493 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1474 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1494 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1475 :relation_type => IssueRelation::TYPE_PRECEDES)
1495 :relation_type => IssueRelation::TYPE_PRECEDES)
1476 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1496 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1477
1497
1478 issue1.reload
1498 issue1.reload
1479 issue1.due_date = '2012-10-23'
1499 issue1.due_date = '2012-10-23'
1480 issue1.save!
1500 issue1.save!
1481 issue2.reload
1501 issue2.reload
1482 assert_equal Date.parse('2012-10-24'), issue2.start_date
1502 assert_equal Date.parse('2012-10-24'), issue2.start_date
1483 assert_equal Date.parse('2012-10-26'), issue2.due_date
1503 assert_equal Date.parse('2012-10-26'), issue2.due_date
1484 end
1504 end
1485
1505
1486 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1506 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1487 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1507 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1488 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1508 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1489 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1509 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1490 :relation_type => IssueRelation::TYPE_PRECEDES)
1510 :relation_type => IssueRelation::TYPE_PRECEDES)
1491 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1511 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1492
1512
1493 issue1.reload
1513 issue1.reload
1494 issue1.start_date = '2012-09-17'
1514 issue1.start_date = '2012-09-17'
1495 issue1.due_date = '2012-09-18'
1515 issue1.due_date = '2012-09-18'
1496 issue1.save!
1516 issue1.save!
1497 issue2.reload
1517 issue2.reload
1498 assert_equal Date.parse('2012-09-19'), issue2.start_date
1518 assert_equal Date.parse('2012-09-19'), issue2.start_date
1499 assert_equal Date.parse('2012-09-21'), issue2.due_date
1519 assert_equal Date.parse('2012-09-21'), issue2.due_date
1500 end
1520 end
1501
1521
1502 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1522 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1503 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1523 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1504 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1524 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1505 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1525 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1506 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1526 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1507 :relation_type => IssueRelation::TYPE_PRECEDES)
1527 :relation_type => IssueRelation::TYPE_PRECEDES)
1508 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1528 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1509 :relation_type => IssueRelation::TYPE_PRECEDES)
1529 :relation_type => IssueRelation::TYPE_PRECEDES)
1510 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1530 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1511
1531
1512 issue1.reload
1532 issue1.reload
1513 issue1.start_date = '2012-09-17'
1533 issue1.start_date = '2012-09-17'
1514 issue1.due_date = '2012-09-18'
1534 issue1.due_date = '2012-09-18'
1515 issue1.save!
1535 issue1.save!
1516 issue2.reload
1536 issue2.reload
1517 # Issue 2 must start after Issue 3
1537 # Issue 2 must start after Issue 3
1518 assert_equal Date.parse('2012-10-03'), issue2.start_date
1538 assert_equal Date.parse('2012-10-03'), issue2.start_date
1519 assert_equal Date.parse('2012-10-05'), issue2.due_date
1539 assert_equal Date.parse('2012-10-05'), issue2.due_date
1520 end
1540 end
1521
1541
1522 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1542 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1523 with_settings :non_working_week_days => [] do
1543 with_settings :non_working_week_days => [] do
1524 stale = Issue.find(1)
1544 stale = Issue.find(1)
1525 issue = Issue.find(1)
1545 issue = Issue.find(1)
1526 issue.subject = "Updated"
1546 issue.subject = "Updated"
1527 issue.save!
1547 issue.save!
1528 date = 10.days.from_now.to_date
1548 date = 10.days.from_now.to_date
1529 assert_nothing_raised do
1549 assert_nothing_raised do
1530 stale.reschedule_on!(date)
1550 stale.reschedule_on!(date)
1531 end
1551 end
1532 assert_equal date, stale.reload.start_date
1552 assert_equal date, stale.reload.start_date
1533 end
1553 end
1534 end
1554 end
1535
1555
1536 def test_child_issue_should_consider_parent_soonest_start_on_create
1556 def test_child_issue_should_consider_parent_soonest_start_on_create
1537 set_language_if_valid 'en'
1557 set_language_if_valid 'en'
1538 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1558 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1539 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1559 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1540 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1560 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1541 :relation_type => IssueRelation::TYPE_PRECEDES)
1561 :relation_type => IssueRelation::TYPE_PRECEDES)
1542 issue1.reload
1562 issue1.reload
1543 issue2.reload
1563 issue2.reload
1544 assert_equal Date.parse('2012-10-18'), issue2.start_date
1564 assert_equal Date.parse('2012-10-18'), issue2.start_date
1545
1565
1546 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1566 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1547 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1567 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1548 assert !child.valid?
1568 assert !child.valid?
1549 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
1569 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
1550 assert_equal Date.parse('2012-10-18'), child.soonest_start
1570 assert_equal Date.parse('2012-10-18'), child.soonest_start
1551 child.start_date = '2012-10-18'
1571 child.start_date = '2012-10-18'
1552 assert child.save
1572 assert child.save
1553 end
1573 end
1554
1574
1555 def test_setting_parent_to_a_dependent_issue_should_not_validate
1575 def test_setting_parent_to_a_dependent_issue_should_not_validate
1556 set_language_if_valid 'en'
1576 set_language_if_valid 'en'
1557 issue1 = Issue.generate!
1577 issue1 = Issue.generate!
1558 issue2 = Issue.generate!
1578 issue2 = Issue.generate!
1559 issue3 = Issue.generate!
1579 issue3 = Issue.generate!
1560 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1580 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1561 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1581 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1562 issue3.reload
1582 issue3.reload
1563 issue3.parent_issue_id = issue2.id
1583 issue3.parent_issue_id = issue2.id
1564 assert !issue3.valid?
1584 assert !issue3.valid?
1565 assert_include 'Parent task is invalid', issue3.errors.full_messages
1585 assert_include 'Parent task is invalid', issue3.errors.full_messages
1566 end
1586 end
1567
1587
1568 def test_setting_parent_should_not_allow_circular_dependency
1588 def test_setting_parent_should_not_allow_circular_dependency
1569 set_language_if_valid 'en'
1589 set_language_if_valid 'en'
1570 issue1 = Issue.generate!
1590 issue1 = Issue.generate!
1571 issue2 = Issue.generate!
1591 issue2 = Issue.generate!
1572 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1592 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1573 issue3 = Issue.generate!
1593 issue3 = Issue.generate!
1574 issue2.reload
1594 issue2.reload
1575 issue2.parent_issue_id = issue3.id
1595 issue2.parent_issue_id = issue3.id
1576 issue2.save!
1596 issue2.save!
1577 issue4 = Issue.generate!
1597 issue4 = Issue.generate!
1578 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1598 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1579 issue4.reload
1599 issue4.reload
1580 issue4.parent_issue_id = issue1.id
1600 issue4.parent_issue_id = issue1.id
1581 assert !issue4.valid?
1601 assert !issue4.valid?
1582 assert_include 'Parent task is invalid', issue4.errors.full_messages
1602 assert_include 'Parent task is invalid', issue4.errors.full_messages
1583 end
1603 end
1584
1604
1585 def test_overdue
1605 def test_overdue
1586 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1606 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1587 assert !Issue.new(:due_date => Date.today).overdue?
1607 assert !Issue.new(:due_date => Date.today).overdue?
1588 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1608 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1589 assert !Issue.new(:due_date => nil).overdue?
1609 assert !Issue.new(:due_date => nil).overdue?
1590 assert !Issue.new(:due_date => 1.day.ago.to_date,
1610 assert !Issue.new(:due_date => 1.day.ago.to_date,
1591 :status => IssueStatus.where(:is_closed => true).first
1611 :status => IssueStatus.where(:is_closed => true).first
1592 ).overdue?
1612 ).overdue?
1593 end
1613 end
1594
1614
1595 test "#behind_schedule? should be false if the issue has no start_date" do
1615 test "#behind_schedule? should be false if the issue has no start_date" do
1596 assert !Issue.new(:start_date => nil,
1616 assert !Issue.new(:start_date => nil,
1597 :due_date => 1.day.from_now.to_date,
1617 :due_date => 1.day.from_now.to_date,
1598 :done_ratio => 0).behind_schedule?
1618 :done_ratio => 0).behind_schedule?
1599 end
1619 end
1600
1620
1601 test "#behind_schedule? should be false if the issue has no end_date" do
1621 test "#behind_schedule? should be false if the issue has no end_date" do
1602 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1622 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1603 :due_date => nil,
1623 :due_date => nil,
1604 :done_ratio => 0).behind_schedule?
1624 :done_ratio => 0).behind_schedule?
1605 end
1625 end
1606
1626
1607 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1627 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1608 assert !Issue.new(:start_date => 50.days.ago.to_date,
1628 assert !Issue.new(:start_date => 50.days.ago.to_date,
1609 :due_date => 50.days.from_now.to_date,
1629 :due_date => 50.days.from_now.to_date,
1610 :done_ratio => 90).behind_schedule?
1630 :done_ratio => 90).behind_schedule?
1611 end
1631 end
1612
1632
1613 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1633 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1614 assert Issue.new(:start_date => 1.day.ago.to_date,
1634 assert Issue.new(:start_date => 1.day.ago.to_date,
1615 :due_date => 1.day.from_now.to_date,
1635 :due_date => 1.day.from_now.to_date,
1616 :done_ratio => 0).behind_schedule?
1636 :done_ratio => 0).behind_schedule?
1617 end
1637 end
1618
1638
1619 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1639 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1620 assert Issue.new(:start_date => 100.days.ago.to_date,
1640 assert Issue.new(:start_date => 100.days.ago.to_date,
1621 :due_date => Date.today,
1641 :due_date => Date.today,
1622 :done_ratio => 90).behind_schedule?
1642 :done_ratio => 90).behind_schedule?
1623 end
1643 end
1624
1644
1625 test "#assignable_users should be Users" do
1645 test "#assignable_users should be Users" do
1626 assert_kind_of User, Issue.find(1).assignable_users.first
1646 assert_kind_of User, Issue.find(1).assignable_users.first
1627 end
1647 end
1628
1648
1629 test "#assignable_users should include the issue author" do
1649 test "#assignable_users should include the issue author" do
1630 non_project_member = User.generate!
1650 non_project_member = User.generate!
1631 issue = Issue.generate!(:author => non_project_member)
1651 issue = Issue.generate!(:author => non_project_member)
1632
1652
1633 assert issue.assignable_users.include?(non_project_member)
1653 assert issue.assignable_users.include?(non_project_member)
1634 end
1654 end
1635
1655
1636 test "#assignable_users should include the current assignee" do
1656 test "#assignable_users should include the current assignee" do
1637 user = User.generate!
1657 user = User.generate!
1638 issue = Issue.generate!(:assigned_to => user)
1658 issue = Issue.generate!(:assigned_to => user)
1639 user.lock!
1659 user.lock!
1640
1660
1641 assert Issue.find(issue.id).assignable_users.include?(user)
1661 assert Issue.find(issue.id).assignable_users.include?(user)
1642 end
1662 end
1643
1663
1644 test "#assignable_users should not show the issue author twice" do
1664 test "#assignable_users should not show the issue author twice" do
1645 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1665 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1646 assert_equal 2, assignable_user_ids.length
1666 assert_equal 2, assignable_user_ids.length
1647
1667
1648 assignable_user_ids.each do |user_id|
1668 assignable_user_ids.each do |user_id|
1649 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1669 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1650 "User #{user_id} appears more or less than once"
1670 "User #{user_id} appears more or less than once"
1651 end
1671 end
1652 end
1672 end
1653
1673
1654 test "#assignable_users with issue_group_assignment should include groups" do
1674 test "#assignable_users with issue_group_assignment should include groups" do
1655 issue = Issue.new(:project => Project.find(2))
1675 issue = Issue.new(:project => Project.find(2))
1656
1676
1657 with_settings :issue_group_assignment => '1' do
1677 with_settings :issue_group_assignment => '1' do
1658 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1678 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1659 assert issue.assignable_users.include?(Group.find(11))
1679 assert issue.assignable_users.include?(Group.find(11))
1660 end
1680 end
1661 end
1681 end
1662
1682
1663 test "#assignable_users without issue_group_assignment should not include groups" do
1683 test "#assignable_users without issue_group_assignment should not include groups" do
1664 issue = Issue.new(:project => Project.find(2))
1684 issue = Issue.new(:project => Project.find(2))
1665
1685
1666 with_settings :issue_group_assignment => '0' do
1686 with_settings :issue_group_assignment => '0' do
1667 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1687 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1668 assert !issue.assignable_users.include?(Group.find(11))
1688 assert !issue.assignable_users.include?(Group.find(11))
1669 end
1689 end
1670 end
1690 end
1671
1691
1672 def test_create_should_send_email_notification
1692 def test_create_should_send_email_notification
1673 ActionMailer::Base.deliveries.clear
1693 ActionMailer::Base.deliveries.clear
1674 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1694 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1675 :author_id => 3, :status_id => 1,
1695 :author_id => 3, :status_id => 1,
1676 :priority => IssuePriority.all.first,
1696 :priority => IssuePriority.all.first,
1677 :subject => 'test_create', :estimated_hours => '1:30')
1697 :subject => 'test_create', :estimated_hours => '1:30')
1678
1698
1679 assert issue.save
1699 assert issue.save
1680 assert_equal 1, ActionMailer::Base.deliveries.size
1700 assert_equal 1, ActionMailer::Base.deliveries.size
1681 end
1701 end
1682
1702
1683 def test_stale_issue_should_not_send_email_notification
1703 def test_stale_issue_should_not_send_email_notification
1684 ActionMailer::Base.deliveries.clear
1704 ActionMailer::Base.deliveries.clear
1685 issue = Issue.find(1)
1705 issue = Issue.find(1)
1686 stale = Issue.find(1)
1706 stale = Issue.find(1)
1687
1707
1688 issue.init_journal(User.find(1))
1708 issue.init_journal(User.find(1))
1689 issue.subject = 'Subjet update'
1709 issue.subject = 'Subjet update'
1690 assert issue.save
1710 assert issue.save
1691 assert_equal 1, ActionMailer::Base.deliveries.size
1711 assert_equal 1, ActionMailer::Base.deliveries.size
1692 ActionMailer::Base.deliveries.clear
1712 ActionMailer::Base.deliveries.clear
1693
1713
1694 stale.init_journal(User.find(1))
1714 stale.init_journal(User.find(1))
1695 stale.subject = 'Another subjet update'
1715 stale.subject = 'Another subjet update'
1696 assert_raise ActiveRecord::StaleObjectError do
1716 assert_raise ActiveRecord::StaleObjectError do
1697 stale.save
1717 stale.save
1698 end
1718 end
1699 assert ActionMailer::Base.deliveries.empty?
1719 assert ActionMailer::Base.deliveries.empty?
1700 end
1720 end
1701
1721
1702 def test_journalized_description
1722 def test_journalized_description
1703 IssueCustomField.delete_all
1723 IssueCustomField.delete_all
1704
1724
1705 i = Issue.first
1725 i = Issue.first
1706 old_description = i.description
1726 old_description = i.description
1707 new_description = "This is the new description"
1727 new_description = "This is the new description"
1708
1728
1709 i.init_journal(User.find(2))
1729 i.init_journal(User.find(2))
1710 i.description = new_description
1730 i.description = new_description
1711 assert_difference 'Journal.count', 1 do
1731 assert_difference 'Journal.count', 1 do
1712 assert_difference 'JournalDetail.count', 1 do
1732 assert_difference 'JournalDetail.count', 1 do
1713 i.save!
1733 i.save!
1714 end
1734 end
1715 end
1735 end
1716
1736
1717 detail = JournalDetail.first(:order => 'id DESC')
1737 detail = JournalDetail.first(:order => 'id DESC')
1718 assert_equal i, detail.journal.journalized
1738 assert_equal i, detail.journal.journalized
1719 assert_equal 'attr', detail.property
1739 assert_equal 'attr', detail.property
1720 assert_equal 'description', detail.prop_key
1740 assert_equal 'description', detail.prop_key
1721 assert_equal old_description, detail.old_value
1741 assert_equal old_description, detail.old_value
1722 assert_equal new_description, detail.value
1742 assert_equal new_description, detail.value
1723 end
1743 end
1724
1744
1725 def test_blank_descriptions_should_not_be_journalized
1745 def test_blank_descriptions_should_not_be_journalized
1726 IssueCustomField.delete_all
1746 IssueCustomField.delete_all
1727 Issue.update_all("description = NULL", "id=1")
1747 Issue.update_all("description = NULL", "id=1")
1728
1748
1729 i = Issue.find(1)
1749 i = Issue.find(1)
1730 i.init_journal(User.find(2))
1750 i.init_journal(User.find(2))
1731 i.subject = "blank description"
1751 i.subject = "blank description"
1732 i.description = "\r\n"
1752 i.description = "\r\n"
1733
1753
1734 assert_difference 'Journal.count', 1 do
1754 assert_difference 'Journal.count', 1 do
1735 assert_difference 'JournalDetail.count', 1 do
1755 assert_difference 'JournalDetail.count', 1 do
1736 i.save!
1756 i.save!
1737 end
1757 end
1738 end
1758 end
1739 end
1759 end
1740
1760
1741 def test_journalized_multi_custom_field
1761 def test_journalized_multi_custom_field
1742 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1762 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1743 :is_filter => true, :is_for_all => true,
1763 :is_filter => true, :is_for_all => true,
1744 :tracker_ids => [1],
1764 :tracker_ids => [1],
1745 :possible_values => ['value1', 'value2', 'value3'],
1765 :possible_values => ['value1', 'value2', 'value3'],
1746 :multiple => true)
1766 :multiple => true)
1747
1767
1748 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1768 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1749 :subject => 'Test', :author_id => 1)
1769 :subject => 'Test', :author_id => 1)
1750
1770
1751 assert_difference 'Journal.count' do
1771 assert_difference 'Journal.count' do
1752 assert_difference 'JournalDetail.count' do
1772 assert_difference 'JournalDetail.count' do
1753 issue.init_journal(User.first)
1773 issue.init_journal(User.first)
1754 issue.custom_field_values = {field.id => ['value1']}
1774 issue.custom_field_values = {field.id => ['value1']}
1755 issue.save!
1775 issue.save!
1756 end
1776 end
1757 assert_difference 'JournalDetail.count' do
1777 assert_difference 'JournalDetail.count' do
1758 issue.init_journal(User.first)
1778 issue.init_journal(User.first)
1759 issue.custom_field_values = {field.id => ['value1', 'value2']}
1779 issue.custom_field_values = {field.id => ['value1', 'value2']}
1760 issue.save!
1780 issue.save!
1761 end
1781 end
1762 assert_difference 'JournalDetail.count', 2 do
1782 assert_difference 'JournalDetail.count', 2 do
1763 issue.init_journal(User.first)
1783 issue.init_journal(User.first)
1764 issue.custom_field_values = {field.id => ['value3', 'value2']}
1784 issue.custom_field_values = {field.id => ['value3', 'value2']}
1765 issue.save!
1785 issue.save!
1766 end
1786 end
1767 assert_difference 'JournalDetail.count', 2 do
1787 assert_difference 'JournalDetail.count', 2 do
1768 issue.init_journal(User.first)
1788 issue.init_journal(User.first)
1769 issue.custom_field_values = {field.id => nil}
1789 issue.custom_field_values = {field.id => nil}
1770 issue.save!
1790 issue.save!
1771 end
1791 end
1772 end
1792 end
1773 end
1793 end
1774
1794
1775 def test_description_eol_should_be_normalized
1795 def test_description_eol_should_be_normalized
1776 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1796 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1777 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1797 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1778 end
1798 end
1779
1799
1780 def test_saving_twice_should_not_duplicate_journal_details
1800 def test_saving_twice_should_not_duplicate_journal_details
1781 i = Issue.first
1801 i = Issue.first
1782 i.init_journal(User.find(2), 'Some notes')
1802 i.init_journal(User.find(2), 'Some notes')
1783 # initial changes
1803 # initial changes
1784 i.subject = 'New subject'
1804 i.subject = 'New subject'
1785 i.done_ratio = i.done_ratio + 10
1805 i.done_ratio = i.done_ratio + 10
1786 assert_difference 'Journal.count' do
1806 assert_difference 'Journal.count' do
1787 assert i.save
1807 assert i.save
1788 end
1808 end
1789 # 1 more change
1809 # 1 more change
1790 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1810 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1791 assert_no_difference 'Journal.count' do
1811 assert_no_difference 'Journal.count' do
1792 assert_difference 'JournalDetail.count', 1 do
1812 assert_difference 'JournalDetail.count', 1 do
1793 i.save
1813 i.save
1794 end
1814 end
1795 end
1815 end
1796 # no more change
1816 # no more change
1797 assert_no_difference 'Journal.count' do
1817 assert_no_difference 'Journal.count' do
1798 assert_no_difference 'JournalDetail.count' do
1818 assert_no_difference 'JournalDetail.count' do
1799 i.save
1819 i.save
1800 end
1820 end
1801 end
1821 end
1802 end
1822 end
1803
1823
1804 def test_all_dependent_issues
1824 def test_all_dependent_issues
1805 IssueRelation.delete_all
1825 IssueRelation.delete_all
1806 assert IssueRelation.create!(:issue_from => Issue.find(1),
1826 assert IssueRelation.create!(:issue_from => Issue.find(1),
1807 :issue_to => Issue.find(2),
1827 :issue_to => Issue.find(2),
1808 :relation_type => IssueRelation::TYPE_PRECEDES)
1828 :relation_type => IssueRelation::TYPE_PRECEDES)
1809 assert IssueRelation.create!(:issue_from => Issue.find(2),
1829 assert IssueRelation.create!(:issue_from => Issue.find(2),
1810 :issue_to => Issue.find(3),
1830 :issue_to => Issue.find(3),
1811 :relation_type => IssueRelation::TYPE_PRECEDES)
1831 :relation_type => IssueRelation::TYPE_PRECEDES)
1812 assert IssueRelation.create!(:issue_from => Issue.find(3),
1832 assert IssueRelation.create!(:issue_from => Issue.find(3),
1813 :issue_to => Issue.find(8),
1833 :issue_to => Issue.find(8),
1814 :relation_type => IssueRelation::TYPE_PRECEDES)
1834 :relation_type => IssueRelation::TYPE_PRECEDES)
1815
1835
1816 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1836 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1817 end
1837 end
1818
1838
1819 def test_all_dependent_issues_with_subtask
1839 def test_all_dependent_issues_with_subtask
1820 IssueRelation.delete_all
1840 IssueRelation.delete_all
1821
1841
1822 project = Project.generate!(:name => "testproject")
1842 project = Project.generate!(:name => "testproject")
1823
1843
1824 parentIssue = Issue.generate!(:project => project)
1844 parentIssue = Issue.generate!(:project => project)
1825 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1845 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1826 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1846 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1827
1847
1828 assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort
1848 assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort
1829 end
1849 end
1830
1850
1831 def test_all_dependent_issues_does_not_include_self
1851 def test_all_dependent_issues_does_not_include_self
1832 IssueRelation.delete_all
1852 IssueRelation.delete_all
1833
1853
1834 project = Project.generate!(:name => "testproject")
1854 project = Project.generate!(:name => "testproject")
1835
1855
1836 parentIssue = Issue.generate!(:project => project)
1856 parentIssue = Issue.generate!(:project => project)
1837 childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1857 childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1838
1858
1839 assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id)
1859 assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id)
1840 end
1860 end
1841
1861
1842 def test_all_dependent_issues_with_parenttask_and_sibling
1862 def test_all_dependent_issues_with_parenttask_and_sibling
1843 IssueRelation.delete_all
1863 IssueRelation.delete_all
1844
1864
1845 project = Project.generate!(:name => "testproject")
1865 project = Project.generate!(:name => "testproject")
1846
1866
1847 parentIssue = Issue.generate!(:project => project)
1867 parentIssue = Issue.generate!(:project => project)
1848 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1868 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1849 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1869 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1850
1870
1851 assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id)
1871 assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id)
1852 end
1872 end
1853
1873
1854 def test_all_dependent_issues_with_relation_to_leaf_in_other_tree
1874 def test_all_dependent_issues_with_relation_to_leaf_in_other_tree
1855 IssueRelation.delete_all
1875 IssueRelation.delete_all
1856
1876
1857 project = Project.generate!(:name => "testproject")
1877 project = Project.generate!(:name => "testproject")
1858
1878
1859 parentIssue1 = Issue.generate!(:project => project)
1879 parentIssue1 = Issue.generate!(:project => project)
1860 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1880 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1861 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1881 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1862
1882
1863 parentIssue2 = Issue.generate!(:project => project)
1883 parentIssue2 = Issue.generate!(:project => project)
1864 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1884 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1865 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1885 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1866
1886
1867
1887
1868 assert IssueRelation.create(:issue_from => parentIssue1,
1888 assert IssueRelation.create(:issue_from => parentIssue1,
1869 :issue_to => childIssue2_2,
1889 :issue_to => childIssue2_2,
1870 :relation_type => IssueRelation::TYPE_BLOCKS)
1890 :relation_type => IssueRelation::TYPE_BLOCKS)
1871
1891
1872 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort,
1892 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort,
1873 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1893 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1874 end
1894 end
1875
1895
1876 def test_all_dependent_issues_with_relation_to_parent_in_other_tree
1896 def test_all_dependent_issues_with_relation_to_parent_in_other_tree
1877 IssueRelation.delete_all
1897 IssueRelation.delete_all
1878
1898
1879 project = Project.generate!(:name => "testproject")
1899 project = Project.generate!(:name => "testproject")
1880
1900
1881 parentIssue1 = Issue.generate!(:project => project)
1901 parentIssue1 = Issue.generate!(:project => project)
1882 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1902 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1883 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1903 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1884
1904
1885 parentIssue2 = Issue.generate!(:project => project)
1905 parentIssue2 = Issue.generate!(:project => project)
1886 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1906 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1887 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1907 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1888
1908
1889
1909
1890 assert IssueRelation.create(:issue_from => parentIssue1,
1910 assert IssueRelation.create(:issue_from => parentIssue1,
1891 :issue_to => parentIssue2,
1911 :issue_to => parentIssue2,
1892 :relation_type => IssueRelation::TYPE_BLOCKS)
1912 :relation_type => IssueRelation::TYPE_BLOCKS)
1893
1913
1894 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort,
1914 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort,
1895 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1915 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1896 end
1916 end
1897
1917
1898 def test_all_dependent_issues_with_transitive_relation
1918 def test_all_dependent_issues_with_transitive_relation
1899 IssueRelation.delete_all
1919 IssueRelation.delete_all
1900
1920
1901 project = Project.generate!(:name => "testproject")
1921 project = Project.generate!(:name => "testproject")
1902
1922
1903 parentIssue1 = Issue.generate!(:project => project)
1923 parentIssue1 = Issue.generate!(:project => project)
1904 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1924 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1905
1925
1906 parentIssue2 = Issue.generate!(:project => project)
1926 parentIssue2 = Issue.generate!(:project => project)
1907 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1927 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1908
1928
1909 independentIssue = Issue.generate!(:project => project)
1929 independentIssue = Issue.generate!(:project => project)
1910
1930
1911 assert IssueRelation.create(:issue_from => parentIssue1,
1931 assert IssueRelation.create(:issue_from => parentIssue1,
1912 :issue_to => childIssue2_1,
1932 :issue_to => childIssue2_1,
1913 :relation_type => IssueRelation::TYPE_RELATES)
1933 :relation_type => IssueRelation::TYPE_RELATES)
1914
1934
1915 assert IssueRelation.create(:issue_from => childIssue2_1,
1935 assert IssueRelation.create(:issue_from => childIssue2_1,
1916 :issue_to => independentIssue,
1936 :issue_to => independentIssue,
1917 :relation_type => IssueRelation::TYPE_RELATES)
1937 :relation_type => IssueRelation::TYPE_RELATES)
1918
1938
1919 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
1939 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
1920 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1940 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1921 end
1941 end
1922
1942
1923 def test_all_dependent_issues_with_transitive_relation2
1943 def test_all_dependent_issues_with_transitive_relation2
1924 IssueRelation.delete_all
1944 IssueRelation.delete_all
1925
1945
1926 project = Project.generate!(:name => "testproject")
1946 project = Project.generate!(:name => "testproject")
1927
1947
1928 parentIssue1 = Issue.generate!(:project => project)
1948 parentIssue1 = Issue.generate!(:project => project)
1929 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1949 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1930
1950
1931 parentIssue2 = Issue.generate!(:project => project)
1951 parentIssue2 = Issue.generate!(:project => project)
1932 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1952 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1933
1953
1934 independentIssue = Issue.generate!(:project => project)
1954 independentIssue = Issue.generate!(:project => project)
1935
1955
1936 assert IssueRelation.create(:issue_from => parentIssue1,
1956 assert IssueRelation.create(:issue_from => parentIssue1,
1937 :issue_to => independentIssue,
1957 :issue_to => independentIssue,
1938 :relation_type => IssueRelation::TYPE_RELATES)
1958 :relation_type => IssueRelation::TYPE_RELATES)
1939
1959
1940 assert IssueRelation.create(:issue_from => independentIssue,
1960 assert IssueRelation.create(:issue_from => independentIssue,
1941 :issue_to => childIssue2_1,
1961 :issue_to => childIssue2_1,
1942 :relation_type => IssueRelation::TYPE_RELATES)
1962 :relation_type => IssueRelation::TYPE_RELATES)
1943
1963
1944 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
1964 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
1945 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1965 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1946
1966
1947 end
1967 end
1948
1968
1949 def test_all_dependent_issues_with_persistent_circular_dependency
1969 def test_all_dependent_issues_with_persistent_circular_dependency
1950 IssueRelation.delete_all
1970 IssueRelation.delete_all
1951 assert IssueRelation.create!(:issue_from => Issue.find(1),
1971 assert IssueRelation.create!(:issue_from => Issue.find(1),
1952 :issue_to => Issue.find(2),
1972 :issue_to => Issue.find(2),
1953 :relation_type => IssueRelation::TYPE_PRECEDES)
1973 :relation_type => IssueRelation::TYPE_PRECEDES)
1954 assert IssueRelation.create!(:issue_from => Issue.find(2),
1974 assert IssueRelation.create!(:issue_from => Issue.find(2),
1955 :issue_to => Issue.find(3),
1975 :issue_to => Issue.find(3),
1956 :relation_type => IssueRelation::TYPE_PRECEDES)
1976 :relation_type => IssueRelation::TYPE_PRECEDES)
1957
1977
1958 r = IssueRelation.create!(:issue_from => Issue.find(3),
1978 r = IssueRelation.create!(:issue_from => Issue.find(3),
1959 :issue_to => Issue.find(7),
1979 :issue_to => Issue.find(7),
1960 :relation_type => IssueRelation::TYPE_PRECEDES)
1980 :relation_type => IssueRelation::TYPE_PRECEDES)
1961 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1981 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1962
1982
1963 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1983 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1964 end
1984 end
1965
1985
1966 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1986 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1967 IssueRelation.delete_all
1987 IssueRelation.delete_all
1968 assert IssueRelation.create!(:issue_from => Issue.find(1),
1988 assert IssueRelation.create!(:issue_from => Issue.find(1),
1969 :issue_to => Issue.find(2),
1989 :issue_to => Issue.find(2),
1970 :relation_type => IssueRelation::TYPE_RELATES)
1990 :relation_type => IssueRelation::TYPE_RELATES)
1971 assert IssueRelation.create!(:issue_from => Issue.find(2),
1991 assert IssueRelation.create!(:issue_from => Issue.find(2),
1972 :issue_to => Issue.find(3),
1992 :issue_to => Issue.find(3),
1973 :relation_type => IssueRelation::TYPE_RELATES)
1993 :relation_type => IssueRelation::TYPE_RELATES)
1974 assert IssueRelation.create!(:issue_from => Issue.find(3),
1994 assert IssueRelation.create!(:issue_from => Issue.find(3),
1975 :issue_to => Issue.find(8),
1995 :issue_to => Issue.find(8),
1976 :relation_type => IssueRelation::TYPE_RELATES)
1996 :relation_type => IssueRelation::TYPE_RELATES)
1977
1997
1978 r = IssueRelation.create!(:issue_from => Issue.find(8),
1998 r = IssueRelation.create!(:issue_from => Issue.find(8),
1979 :issue_to => Issue.find(7),
1999 :issue_to => Issue.find(7),
1980 :relation_type => IssueRelation::TYPE_RELATES)
2000 :relation_type => IssueRelation::TYPE_RELATES)
1981 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
2001 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1982
2002
1983 r = IssueRelation.create!(:issue_from => Issue.find(3),
2003 r = IssueRelation.create!(:issue_from => Issue.find(3),
1984 :issue_to => Issue.find(7),
2004 :issue_to => Issue.find(7),
1985 :relation_type => IssueRelation::TYPE_RELATES)
2005 :relation_type => IssueRelation::TYPE_RELATES)
1986 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
2006 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1987
2007
1988 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
2008 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1989 end
2009 end
1990
2010
1991 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2011 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
1992 @issue = Issue.find(1)
2012 @issue = Issue.find(1)
1993 @issue_status = IssueStatus.find(1)
2013 @issue_status = IssueStatus.find(1)
1994 @issue_status.update_attribute(:default_done_ratio, 50)
2014 @issue_status.update_attribute(:default_done_ratio, 50)
1995 @issue2 = Issue.find(2)
2015 @issue2 = Issue.find(2)
1996 @issue_status2 = IssueStatus.find(2)
2016 @issue_status2 = IssueStatus.find(2)
1997 @issue_status2.update_attribute(:default_done_ratio, 0)
2017 @issue_status2.update_attribute(:default_done_ratio, 0)
1998
2018
1999 with_settings :issue_done_ratio => 'issue_field' do
2019 with_settings :issue_done_ratio => 'issue_field' do
2000 assert_equal 0, @issue.done_ratio
2020 assert_equal 0, @issue.done_ratio
2001 assert_equal 30, @issue2.done_ratio
2021 assert_equal 30, @issue2.done_ratio
2002 end
2022 end
2003
2023
2004 with_settings :issue_done_ratio => 'issue_status' do
2024 with_settings :issue_done_ratio => 'issue_status' do
2005 assert_equal 50, @issue.done_ratio
2025 assert_equal 50, @issue.done_ratio
2006 assert_equal 0, @issue2.done_ratio
2026 assert_equal 0, @issue2.done_ratio
2007 end
2027 end
2008 end
2028 end
2009
2029
2010 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2030 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2011 @issue = Issue.find(1)
2031 @issue = Issue.find(1)
2012 @issue_status = IssueStatus.find(1)
2032 @issue_status = IssueStatus.find(1)
2013 @issue_status.update_attribute(:default_done_ratio, 50)
2033 @issue_status.update_attribute(:default_done_ratio, 50)
2014 @issue2 = Issue.find(2)
2034 @issue2 = Issue.find(2)
2015 @issue_status2 = IssueStatus.find(2)
2035 @issue_status2 = IssueStatus.find(2)
2016 @issue_status2.update_attribute(:default_done_ratio, 0)
2036 @issue_status2.update_attribute(:default_done_ratio, 0)
2017
2037
2018 with_settings :issue_done_ratio => 'issue_field' do
2038 with_settings :issue_done_ratio => 'issue_field' do
2019 @issue.update_done_ratio_from_issue_status
2039 @issue.update_done_ratio_from_issue_status
2020 @issue2.update_done_ratio_from_issue_status
2040 @issue2.update_done_ratio_from_issue_status
2021
2041
2022 assert_equal 0, @issue.read_attribute(:done_ratio)
2042 assert_equal 0, @issue.read_attribute(:done_ratio)
2023 assert_equal 30, @issue2.read_attribute(:done_ratio)
2043 assert_equal 30, @issue2.read_attribute(:done_ratio)
2024 end
2044 end
2025
2045
2026 with_settings :issue_done_ratio => 'issue_status' do
2046 with_settings :issue_done_ratio => 'issue_status' do
2027 @issue.update_done_ratio_from_issue_status
2047 @issue.update_done_ratio_from_issue_status
2028 @issue2.update_done_ratio_from_issue_status
2048 @issue2.update_done_ratio_from_issue_status
2029
2049
2030 assert_equal 50, @issue.read_attribute(:done_ratio)
2050 assert_equal 50, @issue.read_attribute(:done_ratio)
2031 assert_equal 0, @issue2.read_attribute(:done_ratio)
2051 assert_equal 0, @issue2.read_attribute(:done_ratio)
2032 end
2052 end
2033 end
2053 end
2034
2054
2035 test "#by_tracker" do
2055 test "#by_tracker" do
2036 User.current = User.anonymous
2056 User.current = User.anonymous
2037 groups = Issue.by_tracker(Project.find(1))
2057 groups = Issue.by_tracker(Project.find(1))
2038 assert_equal 3, groups.count
2058 assert_equal 3, groups.count
2039 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2059 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2040 end
2060 end
2041
2061
2042 test "#by_version" do
2062 test "#by_version" do
2043 User.current = User.anonymous
2063 User.current = User.anonymous
2044 groups = Issue.by_version(Project.find(1))
2064 groups = Issue.by_version(Project.find(1))
2045 assert_equal 3, groups.count
2065 assert_equal 3, groups.count
2046 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2066 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2047 end
2067 end
2048
2068
2049 test "#by_priority" do
2069 test "#by_priority" do
2050 User.current = User.anonymous
2070 User.current = User.anonymous
2051 groups = Issue.by_priority(Project.find(1))
2071 groups = Issue.by_priority(Project.find(1))
2052 assert_equal 4, groups.count
2072 assert_equal 4, groups.count
2053 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2073 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2054 end
2074 end
2055
2075
2056 test "#by_category" do
2076 test "#by_category" do
2057 User.current = User.anonymous
2077 User.current = User.anonymous
2058 groups = Issue.by_category(Project.find(1))
2078 groups = Issue.by_category(Project.find(1))
2059 assert_equal 2, groups.count
2079 assert_equal 2, groups.count
2060 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2080 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2061 end
2081 end
2062
2082
2063 test "#by_assigned_to" do
2083 test "#by_assigned_to" do
2064 User.current = User.anonymous
2084 User.current = User.anonymous
2065 groups = Issue.by_assigned_to(Project.find(1))
2085 groups = Issue.by_assigned_to(Project.find(1))
2066 assert_equal 2, groups.count
2086 assert_equal 2, groups.count
2067 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2087 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2068 end
2088 end
2069
2089
2070 test "#by_author" do
2090 test "#by_author" do
2071 User.current = User.anonymous
2091 User.current = User.anonymous
2072 groups = Issue.by_author(Project.find(1))
2092 groups = Issue.by_author(Project.find(1))
2073 assert_equal 4, groups.count
2093 assert_equal 4, groups.count
2074 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2094 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2075 end
2095 end
2076
2096
2077 test "#by_subproject" do
2097 test "#by_subproject" do
2078 User.current = User.anonymous
2098 User.current = User.anonymous
2079 groups = Issue.by_subproject(Project.find(1))
2099 groups = Issue.by_subproject(Project.find(1))
2080 # Private descendant not visible
2100 # Private descendant not visible
2081 assert_equal 1, groups.count
2101 assert_equal 1, groups.count
2082 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2102 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2083 end
2103 end
2084
2104
2085 def test_recently_updated_scope
2105 def test_recently_updated_scope
2086 #should return the last updated issue
2106 #should return the last updated issue
2087 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2107 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2088 end
2108 end
2089
2109
2090 def test_on_active_projects_scope
2110 def test_on_active_projects_scope
2091 assert Project.find(2).archive
2111 assert Project.find(2).archive
2092
2112
2093 before = Issue.on_active_project.length
2113 before = Issue.on_active_project.length
2094 # test inclusion to results
2114 # test inclusion to results
2095 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2115 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2096 assert_equal before + 1, Issue.on_active_project.length
2116 assert_equal before + 1, Issue.on_active_project.length
2097
2117
2098 # Move to an archived project
2118 # Move to an archived project
2099 issue.project = Project.find(2)
2119 issue.project = Project.find(2)
2100 assert issue.save
2120 assert issue.save
2101 assert_equal before, Issue.on_active_project.length
2121 assert_equal before, Issue.on_active_project.length
2102 end
2122 end
2103
2123
2104 test "Issue#recipients should include project recipients" do
2124 test "Issue#recipients should include project recipients" do
2105 issue = Issue.generate!
2125 issue = Issue.generate!
2106 assert issue.project.recipients.present?
2126 assert issue.project.recipients.present?
2107 issue.project.recipients.each do |project_recipient|
2127 issue.project.recipients.each do |project_recipient|
2108 assert issue.recipients.include?(project_recipient)
2128 assert issue.recipients.include?(project_recipient)
2109 end
2129 end
2110 end
2130 end
2111
2131
2112 test "Issue#recipients should include the author if the author is active" do
2132 test "Issue#recipients should include the author if the author is active" do
2113 issue = Issue.generate!(:author => User.generate!)
2133 issue = Issue.generate!(:author => User.generate!)
2114 assert issue.author, "No author set for Issue"
2134 assert issue.author, "No author set for Issue"
2115 assert issue.recipients.include?(issue.author.mail)
2135 assert issue.recipients.include?(issue.author.mail)
2116 end
2136 end
2117
2137
2118 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2138 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2119 issue = Issue.generate!(:assigned_to => User.generate!)
2139 issue = Issue.generate!(:assigned_to => User.generate!)
2120 assert issue.assigned_to, "No assigned_to set for Issue"
2140 assert issue.assigned_to, "No assigned_to set for Issue"
2121 assert issue.recipients.include?(issue.assigned_to.mail)
2141 assert issue.recipients.include?(issue.assigned_to.mail)
2122 end
2142 end
2123
2143
2124 test "Issue#recipients should not include users who opt out of all email" do
2144 test "Issue#recipients should not include users who opt out of all email" do
2125 issue = Issue.generate!(:author => User.generate!)
2145 issue = Issue.generate!(:author => User.generate!)
2126 issue.author.update_attribute(:mail_notification, :none)
2146 issue.author.update_attribute(:mail_notification, :none)
2127 assert !issue.recipients.include?(issue.author.mail)
2147 assert !issue.recipients.include?(issue.author.mail)
2128 end
2148 end
2129
2149
2130 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2150 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2131 issue = Issue.generate!(:author => User.generate!)
2151 issue = Issue.generate!(:author => User.generate!)
2132 issue.author.update_attribute(:mail_notification, :only_assigned)
2152 issue.author.update_attribute(:mail_notification, :only_assigned)
2133 assert !issue.recipients.include?(issue.author.mail)
2153 assert !issue.recipients.include?(issue.author.mail)
2134 end
2154 end
2135
2155
2136 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2156 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2137 issue = Issue.generate!(:assigned_to => User.generate!)
2157 issue = Issue.generate!(:assigned_to => User.generate!)
2138 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2158 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2139 assert !issue.recipients.include?(issue.assigned_to.mail)
2159 assert !issue.recipients.include?(issue.assigned_to.mail)
2140 end
2160 end
2141
2161
2142 def test_last_journal_id_with_journals_should_return_the_journal_id
2162 def test_last_journal_id_with_journals_should_return_the_journal_id
2143 assert_equal 2, Issue.find(1).last_journal_id
2163 assert_equal 2, Issue.find(1).last_journal_id
2144 end
2164 end
2145
2165
2146 def test_last_journal_id_without_journals_should_return_nil
2166 def test_last_journal_id_without_journals_should_return_nil
2147 assert_nil Issue.find(3).last_journal_id
2167 assert_nil Issue.find(3).last_journal_id
2148 end
2168 end
2149
2169
2150 def test_journals_after_should_return_journals_with_greater_id
2170 def test_journals_after_should_return_journals_with_greater_id
2151 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2171 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2152 assert_equal [], Issue.find(1).journals_after('2')
2172 assert_equal [], Issue.find(1).journals_after('2')
2153 end
2173 end
2154
2174
2155 def test_journals_after_with_blank_arg_should_return_all_journals
2175 def test_journals_after_with_blank_arg_should_return_all_journals
2156 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2176 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2157 end
2177 end
2158
2178
2159 def test_css_classes_should_include_tracker
2179 def test_css_classes_should_include_tracker
2160 issue = Issue.new(:tracker => Tracker.find(2))
2180 issue = Issue.new(:tracker => Tracker.find(2))
2161 classes = issue.css_classes.split(' ')
2181 classes = issue.css_classes.split(' ')
2162 assert_include 'tracker-2', classes
2182 assert_include 'tracker-2', classes
2163 end
2183 end
2164
2184
2165 def test_css_classes_should_include_priority
2185 def test_css_classes_should_include_priority
2166 issue = Issue.new(:priority => IssuePriority.find(8))
2186 issue = Issue.new(:priority => IssuePriority.find(8))
2167 classes = issue.css_classes.split(' ')
2187 classes = issue.css_classes.split(' ')
2168 assert_include 'priority-8', classes
2188 assert_include 'priority-8', classes
2169 assert_include 'priority-highest', classes
2189 assert_include 'priority-highest', classes
2170 end
2190 end
2171
2191
2172 def test_css_classes_should_include_user_assignment
2192 def test_css_classes_should_include_user_assignment
2173 issue = Issue.generate(:assigned_to_id => 2)
2193 issue = Issue.generate(:assigned_to_id => 2)
2174 assert_include 'assigned-to-me', issue.css_classes(User.find(2))
2194 assert_include 'assigned-to-me', issue.css_classes(User.find(2))
2175 assert_not_include 'assigned-to-me', issue.css_classes(User.find(3))
2195 assert_not_include 'assigned-to-me', issue.css_classes(User.find(3))
2176 end
2196 end
2177
2197
2178 def test_css_classes_should_include_user_group_assignment
2198 def test_css_classes_should_include_user_group_assignment
2179 issue = Issue.generate(:assigned_to_id => 10)
2199 issue = Issue.generate(:assigned_to_id => 10)
2180 assert_include 'assigned-to-my-group', issue.css_classes(Group.find(10).users.first)
2200 assert_include 'assigned-to-my-group', issue.css_classes(Group.find(10).users.first)
2181 assert_not_include 'assigned-to-my-group', issue.css_classes(User.find(3))
2201 assert_not_include 'assigned-to-my-group', issue.css_classes(User.find(3))
2182 end
2202 end
2183
2203
2184 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2204 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2185 set_tmp_attachments_directory
2205 set_tmp_attachments_directory
2186 issue = Issue.generate!
2206 issue = Issue.generate!
2187 issue.save_attachments({
2207 issue.save_attachments({
2188 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2208 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2189 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2209 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2190 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2210 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2191 })
2211 })
2192 issue.attach_saved_attachments
2212 issue.attach_saved_attachments
2193
2213
2194 assert_equal 3, issue.reload.attachments.count
2214 assert_equal 3, issue.reload.attachments.count
2195 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2215 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2196 end
2216 end
2197
2217
2198 def test_closed_on_should_be_nil_when_creating_an_open_issue
2218 def test_closed_on_should_be_nil_when_creating_an_open_issue
2199 issue = Issue.generate!(:status_id => 1).reload
2219 issue = Issue.generate!(:status_id => 1).reload
2200 assert !issue.closed?
2220 assert !issue.closed?
2201 assert_nil issue.closed_on
2221 assert_nil issue.closed_on
2202 end
2222 end
2203
2223
2204 def test_closed_on_should_be_set_when_creating_a_closed_issue
2224 def test_closed_on_should_be_set_when_creating_a_closed_issue
2205 issue = Issue.generate!(:status_id => 5).reload
2225 issue = Issue.generate!(:status_id => 5).reload
2206 assert issue.closed?
2226 assert issue.closed?
2207 assert_not_nil issue.closed_on
2227 assert_not_nil issue.closed_on
2208 assert_equal issue.updated_on, issue.closed_on
2228 assert_equal issue.updated_on, issue.closed_on
2209 assert_equal issue.created_on, issue.closed_on
2229 assert_equal issue.created_on, issue.closed_on
2210 end
2230 end
2211
2231
2212 def test_closed_on_should_be_nil_when_updating_an_open_issue
2232 def test_closed_on_should_be_nil_when_updating_an_open_issue
2213 issue = Issue.find(1)
2233 issue = Issue.find(1)
2214 issue.subject = 'Not closed yet'
2234 issue.subject = 'Not closed yet'
2215 issue.save!
2235 issue.save!
2216 issue.reload
2236 issue.reload
2217 assert_nil issue.closed_on
2237 assert_nil issue.closed_on
2218 end
2238 end
2219
2239
2220 def test_closed_on_should_be_set_when_closing_an_open_issue
2240 def test_closed_on_should_be_set_when_closing_an_open_issue
2221 issue = Issue.find(1)
2241 issue = Issue.find(1)
2222 issue.subject = 'Now closed'
2242 issue.subject = 'Now closed'
2223 issue.status_id = 5
2243 issue.status_id = 5
2224 issue.save!
2244 issue.save!
2225 issue.reload
2245 issue.reload
2226 assert_not_nil issue.closed_on
2246 assert_not_nil issue.closed_on
2227 assert_equal issue.updated_on, issue.closed_on
2247 assert_equal issue.updated_on, issue.closed_on
2228 end
2248 end
2229
2249
2230 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2250 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2231 issue = Issue.open(false).first
2251 issue = Issue.open(false).first
2232 was_closed_on = issue.closed_on
2252 was_closed_on = issue.closed_on
2233 assert_not_nil was_closed_on
2253 assert_not_nil was_closed_on
2234 issue.subject = 'Updating a closed issue'
2254 issue.subject = 'Updating a closed issue'
2235 issue.save!
2255 issue.save!
2236 issue.reload
2256 issue.reload
2237 assert_equal was_closed_on, issue.closed_on
2257 assert_equal was_closed_on, issue.closed_on
2238 end
2258 end
2239
2259
2240 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2260 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2241 issue = Issue.open(false).first
2261 issue = Issue.open(false).first
2242 was_closed_on = issue.closed_on
2262 was_closed_on = issue.closed_on
2243 assert_not_nil was_closed_on
2263 assert_not_nil was_closed_on
2244 issue.subject = 'Reopening a closed issue'
2264 issue.subject = 'Reopening a closed issue'
2245 issue.status_id = 1
2265 issue.status_id = 1
2246 issue.save!
2266 issue.save!
2247 issue.reload
2267 issue.reload
2248 assert !issue.closed?
2268 assert !issue.closed?
2249 assert_equal was_closed_on, issue.closed_on
2269 assert_equal was_closed_on, issue.closed_on
2250 end
2270 end
2251
2271
2252 def test_status_was_should_return_nil_for_new_issue
2272 def test_status_was_should_return_nil_for_new_issue
2253 issue = Issue.new
2273 issue = Issue.new
2254 assert_nil issue.status_was
2274 assert_nil issue.status_was
2255 end
2275 end
2256
2276
2257 def test_status_was_should_return_status_before_change
2277 def test_status_was_should_return_status_before_change
2258 issue = Issue.find(1)
2278 issue = Issue.find(1)
2259 issue.status = IssueStatus.find(2)
2279 issue.status = IssueStatus.find(2)
2260 assert_equal IssueStatus.find(1), issue.status_was
2280 assert_equal IssueStatus.find(1), issue.status_was
2261 end
2281 end
2262
2282
2263 def test_status_was_should_be_reset_on_save
2283 def test_status_was_should_be_reset_on_save
2264 issue = Issue.find(1)
2284 issue = Issue.find(1)
2265 issue.status = IssueStatus.find(2)
2285 issue.status = IssueStatus.find(2)
2266 assert_equal IssueStatus.find(1), issue.status_was
2286 assert_equal IssueStatus.find(1), issue.status_was
2267 assert issue.save!
2287 assert issue.save!
2268 assert_equal IssueStatus.find(2), issue.status_was
2288 assert_equal IssueStatus.find(2), issue.status_was
2269 end
2289 end
2270 end
2290 end
General Comments 0
You need to be logged in to leave comments. Login now