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