##// END OF EJS Templates
Fixed: No validation errors when entering an invalid "Parent task" (#11979)....
Jean-Philippe Lang -
r10404:2b797fa82fdd
parent child
Show More

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

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