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