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