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