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