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