##// END OF EJS Templates
Fixed: Circular loop when using relations and subtasks (#8794)....
Jean-Philippe Lang -
r11411:38b3e045cf22
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

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