##// END OF EJS Templates
Merged r6147 and r6149 from trunk....
Jean-Philippe Lang -
r6040:8e87cd6f772a
parent child
Show More
@@ -1,920 +1,930
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 => 'User', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'User', :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_remove => :attachment_removed
38 acts_as_attachable :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
61
62 named_scope :visible, lambda {|*args| { :include => :project,
62 named_scope :visible, lambda {|*args| { :include => :project,
63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
63 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64
64
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66
66
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71
71
72 named_scope :without_version, lambda {
72 named_scope :without_version, lambda {
73 {
73 {
74 :conditions => { :fixed_version_id => nil}
74 :conditions => { :fixed_version_id => nil}
75 }
75 }
76 }
76 }
77
77
78 named_scope :with_query, lambda {|query|
78 named_scope :with_query, lambda {|query|
79 {
79 {
80 :conditions => Query.merge_conditions(query.statement)
80 :conditions => Query.merge_conditions(query.statement)
81 }
81 }
82 }
82 }
83
83
84 before_create :default_assign
84 before_create :default_assign
85 before_save :close_duplicates, :update_done_ratio_from_issue_status
85 before_save :close_duplicates, :update_done_ratio_from_issue_status
86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
86 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
87 after_destroy :update_parent_attributes
87 after_destroy :update_parent_attributes
88
88
89 # Returns a SQL conditions string used to find all issues visible by the specified user
89 # Returns a SQL conditions string used to find all issues visible by the specified user
90 def self.visible_condition(user, options={})
90 def self.visible_condition(user, options={})
91 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
91 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
92 case role.issues_visibility
92 case role.issues_visibility
93 when 'all'
93 when 'all'
94 nil
94 nil
95 when 'default'
95 when 'default'
96 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
96 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
97 when 'own'
97 when 'own'
98 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
98 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id = #{user.id})"
99 else
99 else
100 '1=0'
100 '1=0'
101 end
101 end
102 end
102 end
103 end
103 end
104
104
105 # Returns true if usr or current user is allowed to view the issue
105 # Returns true if usr or current user is allowed to view the issue
106 def visible?(usr=nil)
106 def visible?(usr=nil)
107 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
107 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
108 case role.issues_visibility
108 case role.issues_visibility
109 when 'all'
109 when 'all'
110 true
110 true
111 when 'default'
111 when 'default'
112 !self.is_private? || self.author == user || self.assigned_to == user
112 !self.is_private? || self.author == user || self.assigned_to == user
113 when 'own'
113 when 'own'
114 self.author == user || self.assigned_to == user
114 self.author == user || self.assigned_to == user
115 else
115 else
116 false
116 false
117 end
117 end
118 end
118 end
119 end
119 end
120
120
121 def after_initialize
121 def after_initialize
122 if new_record?
122 if new_record?
123 # set default values for new records only
123 # set default values for new records only
124 self.status ||= IssueStatus.default
124 self.status ||= IssueStatus.default
125 self.priority ||= IssuePriority.default
125 self.priority ||= IssuePriority.default
126 end
126 end
127 end
127 end
128
128
129 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
129 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
130 def available_custom_fields
130 def available_custom_fields
131 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
131 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
132 end
132 end
133
133
134 def copy_from(arg)
134 def copy_from(arg)
135 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
135 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
136 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
136 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
137 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
137 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
138 self.status = issue.status
138 self.status = issue.status
139 self
139 self
140 end
140 end
141
141
142 # Moves/copies an issue to a new project and tracker
142 # Moves/copies an issue to a new project and tracker
143 # Returns the moved/copied issue on success, false on failure
143 # Returns the moved/copied issue on success, false on failure
144 def move_to_project(*args)
144 def move_to_project(*args)
145 ret = Issue.transaction do
145 ret = Issue.transaction do
146 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
146 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
147 end || false
147 end || false
148 end
148 end
149
149
150 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
150 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
151 options ||= {}
151 options ||= {}
152 issue = options[:copy] ? self.class.new.copy_from(self) : self
152 issue = options[:copy] ? self.class.new.copy_from(self) : self
153
153
154 if new_project && issue.project_id != new_project.id
154 if new_project && issue.project_id != new_project.id
155 # delete issue relations
155 # delete issue relations
156 unless Setting.cross_project_issue_relations?
156 unless Setting.cross_project_issue_relations?
157 issue.relations_from.clear
157 issue.relations_from.clear
158 issue.relations_to.clear
158 issue.relations_to.clear
159 end
159 end
160 # issue is moved to another project
160 # issue is moved to another project
161 # reassign to the category with same name if any
161 # reassign to the category with same name if any
162 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
162 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
163 issue.category = new_category
163 issue.category = new_category
164 # Keep the fixed_version if it's still valid in the new_project
164 # Keep the fixed_version if it's still valid in the new_project
165 unless new_project.shared_versions.include?(issue.fixed_version)
165 unless new_project.shared_versions.include?(issue.fixed_version)
166 issue.fixed_version = nil
166 issue.fixed_version = nil
167 end
167 end
168 issue.project = new_project
168 issue.project = new_project
169 if issue.parent && issue.parent.project_id != issue.project_id
169 if issue.parent && issue.parent.project_id != issue.project_id
170 issue.parent_issue_id = nil
170 issue.parent_issue_id = nil
171 end
171 end
172 end
172 end
173 if new_tracker
173 if new_tracker
174 issue.tracker = new_tracker
174 issue.tracker = new_tracker
175 issue.reset_custom_values!
175 issue.reset_custom_values!
176 end
176 end
177 if options[:copy]
177 if options[:copy]
178 issue.author = User.current
178 issue.author = User.current
179 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
179 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
180 issue.status = if options[:attributes] && options[:attributes][:status_id]
180 issue.status = if options[:attributes] && options[:attributes][:status_id]
181 IssueStatus.find_by_id(options[:attributes][:status_id])
181 IssueStatus.find_by_id(options[:attributes][:status_id])
182 else
182 else
183 self.status
183 self.status
184 end
184 end
185 end
185 end
186 # Allow bulk setting of attributes on the issue
186 # Allow bulk setting of attributes on the issue
187 if options[:attributes]
187 if options[:attributes]
188 issue.attributes = options[:attributes]
188 issue.attributes = options[:attributes]
189 end
189 end
190 if issue.save
190 if issue.save
191 if options[:copy]
191 if options[:copy]
192 if current_journal && current_journal.notes.present?
192 if current_journal && current_journal.notes.present?
193 issue.init_journal(current_journal.user, current_journal.notes)
193 issue.init_journal(current_journal.user, current_journal.notes)
194 issue.current_journal.notify = false
194 issue.current_journal.notify = false
195 issue.save
195 issue.save
196 end
196 end
197 else
197 else
198 # Manually update project_id on related time entries
198 # Manually update project_id on related time entries
199 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
199 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
200
200
201 issue.children.each do |child|
201 issue.children.each do |child|
202 unless child.move_to_project_without_transaction(new_project)
202 unless child.move_to_project_without_transaction(new_project)
203 # Move failed and transaction was rollback'd
203 # Move failed and transaction was rollback'd
204 return false
204 return false
205 end
205 end
206 end
206 end
207 end
207 end
208 else
208 else
209 return false
209 return false
210 end
210 end
211 issue
211 issue
212 end
212 end
213
213
214 def status_id=(sid)
214 def status_id=(sid)
215 self.status = nil
215 self.status = nil
216 write_attribute(:status_id, sid)
216 write_attribute(:status_id, sid)
217 end
217 end
218
218
219 def priority_id=(pid)
219 def priority_id=(pid)
220 self.priority = nil
220 self.priority = nil
221 write_attribute(:priority_id, pid)
221 write_attribute(:priority_id, pid)
222 end
222 end
223
223
224 def tracker_id=(tid)
224 def tracker_id=(tid)
225 self.tracker = nil
225 self.tracker = nil
226 result = write_attribute(:tracker_id, tid)
226 result = write_attribute(:tracker_id, tid)
227 @custom_field_values = nil
227 @custom_field_values = nil
228 result
228 result
229 end
229 end
230
230
231 def description=(arg)
232 if arg.is_a?(String)
233 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
234 end
235 write_attribute(:description, arg)
236 end
237
231 # Overrides attributes= so that tracker_id gets assigned first
238 # Overrides attributes= so that tracker_id gets assigned first
232 def attributes_with_tracker_first=(new_attributes, *args)
239 def attributes_with_tracker_first=(new_attributes, *args)
233 return if new_attributes.nil?
240 return if new_attributes.nil?
234 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
241 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
235 if new_tracker_id
242 if new_tracker_id
236 self.tracker_id = new_tracker_id
243 self.tracker_id = new_tracker_id
237 end
244 end
238 send :attributes_without_tracker_first=, new_attributes, *args
245 send :attributes_without_tracker_first=, new_attributes, *args
239 end
246 end
240 # Do not redefine alias chain on reload (see #4838)
247 # Do not redefine alias chain on reload (see #4838)
241 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
248 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
242
249
243 def estimated_hours=(h)
250 def estimated_hours=(h)
244 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
251 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
245 end
252 end
246
253
247 safe_attributes 'tracker_id',
254 safe_attributes 'tracker_id',
248 'status_id',
255 'status_id',
249 'parent_issue_id',
256 'parent_issue_id',
250 'category_id',
257 'category_id',
251 'assigned_to_id',
258 'assigned_to_id',
252 'priority_id',
259 'priority_id',
253 'fixed_version_id',
260 'fixed_version_id',
254 'subject',
261 'subject',
255 'description',
262 'description',
256 'start_date',
263 'start_date',
257 'due_date',
264 'due_date',
258 'done_ratio',
265 'done_ratio',
259 'estimated_hours',
266 'estimated_hours',
260 'custom_field_values',
267 'custom_field_values',
261 'custom_fields',
268 'custom_fields',
262 'lock_version',
269 'lock_version',
263 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
270 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
264
271
265 safe_attributes 'status_id',
272 safe_attributes 'status_id',
266 'assigned_to_id',
273 'assigned_to_id',
267 'fixed_version_id',
274 'fixed_version_id',
268 'done_ratio',
275 'done_ratio',
269 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
276 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
270
277
271 safe_attributes 'is_private',
278 safe_attributes 'is_private',
272 :if => lambda {|issue, user|
279 :if => lambda {|issue, user|
273 user.allowed_to?(:set_issues_private, issue.project) ||
280 user.allowed_to?(:set_issues_private, issue.project) ||
274 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
281 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
275 }
282 }
276
283
277 # Safely sets attributes
284 # Safely sets attributes
278 # Should be called from controllers instead of #attributes=
285 # Should be called from controllers instead of #attributes=
279 # attr_accessible is too rough because we still want things like
286 # attr_accessible is too rough because we still want things like
280 # Issue.new(:project => foo) to work
287 # Issue.new(:project => foo) to work
281 # TODO: move workflow/permission checks from controllers to here
288 # TODO: move workflow/permission checks from controllers to here
282 def safe_attributes=(attrs, user=User.current)
289 def safe_attributes=(attrs, user=User.current)
283 return unless attrs.is_a?(Hash)
290 return unless attrs.is_a?(Hash)
284
291
285 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
292 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
286 attrs = delete_unsafe_attributes(attrs, user)
293 attrs = delete_unsafe_attributes(attrs, user)
287 return if attrs.empty?
294 return if attrs.empty?
288
295
289 # Tracker must be set before since new_statuses_allowed_to depends on it.
296 # Tracker must be set before since new_statuses_allowed_to depends on it.
290 if t = attrs.delete('tracker_id')
297 if t = attrs.delete('tracker_id')
291 self.tracker_id = t
298 self.tracker_id = t
292 end
299 end
293
300
294 if attrs['status_id']
301 if attrs['status_id']
295 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
302 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
296 attrs.delete('status_id')
303 attrs.delete('status_id')
297 end
304 end
298 end
305 end
299
306
300 unless leaf?
307 unless leaf?
301 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
308 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
302 end
309 end
303
310
304 if attrs.has_key?('parent_issue_id')
311 if attrs.has_key?('parent_issue_id')
305 if !user.allowed_to?(:manage_subtasks, project)
312 if !user.allowed_to?(:manage_subtasks, project)
306 attrs.delete('parent_issue_id')
313 attrs.delete('parent_issue_id')
307 elsif !attrs['parent_issue_id'].blank?
314 elsif !attrs['parent_issue_id'].blank?
308 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
315 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
309 end
316 end
310 end
317 end
311
318
312 self.attributes = attrs
319 self.attributes = attrs
313 end
320 end
314
321
315 def done_ratio
322 def done_ratio
316 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
323 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
317 status.default_done_ratio
324 status.default_done_ratio
318 else
325 else
319 read_attribute(:done_ratio)
326 read_attribute(:done_ratio)
320 end
327 end
321 end
328 end
322
329
323 def self.use_status_for_done_ratio?
330 def self.use_status_for_done_ratio?
324 Setting.issue_done_ratio == 'issue_status'
331 Setting.issue_done_ratio == 'issue_status'
325 end
332 end
326
333
327 def self.use_field_for_done_ratio?
334 def self.use_field_for_done_ratio?
328 Setting.issue_done_ratio == 'issue_field'
335 Setting.issue_done_ratio == 'issue_field'
329 end
336 end
330
337
331 def validate
338 def validate
332 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
339 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
333 errors.add :due_date, :not_a_date
340 errors.add :due_date, :not_a_date
334 end
341 end
335
342
336 if self.due_date and self.start_date and self.due_date < self.start_date
343 if self.due_date and self.start_date and self.due_date < self.start_date
337 errors.add :due_date, :greater_than_start_date
344 errors.add :due_date, :greater_than_start_date
338 end
345 end
339
346
340 if start_date && soonest_start && start_date < soonest_start
347 if start_date && soonest_start && start_date < soonest_start
341 errors.add :start_date, :invalid
348 errors.add :start_date, :invalid
342 end
349 end
343
350
344 if fixed_version
351 if fixed_version
345 if !assignable_versions.include?(fixed_version)
352 if !assignable_versions.include?(fixed_version)
346 errors.add :fixed_version_id, :inclusion
353 errors.add :fixed_version_id, :inclusion
347 elsif reopened? && fixed_version.closed?
354 elsif reopened? && fixed_version.closed?
348 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
355 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
349 end
356 end
350 end
357 end
351
358
352 # Checks that the issue can not be added/moved to a disabled tracker
359 # Checks that the issue can not be added/moved to a disabled tracker
353 if project && (tracker_id_changed? || project_id_changed?)
360 if project && (tracker_id_changed? || project_id_changed?)
354 unless project.trackers.include?(tracker)
361 unless project.trackers.include?(tracker)
355 errors.add :tracker_id, :inclusion
362 errors.add :tracker_id, :inclusion
356 end
363 end
357 end
364 end
358
365
359 # Checks parent issue assignment
366 # Checks parent issue assignment
360 if @parent_issue
367 if @parent_issue
361 if @parent_issue.project_id != project_id
368 if @parent_issue.project_id != project_id
362 errors.add :parent_issue_id, :not_same_project
369 errors.add :parent_issue_id, :not_same_project
363 elsif !new_record?
370 elsif !new_record?
364 # moving an existing issue
371 # moving an existing issue
365 if @parent_issue.root_id != root_id
372 if @parent_issue.root_id != root_id
366 # we can always move to another tree
373 # we can always move to another tree
367 elsif move_possible?(@parent_issue)
374 elsif move_possible?(@parent_issue)
368 # move accepted inside tree
375 # move accepted inside tree
369 else
376 else
370 errors.add :parent_issue_id, :not_a_valid_parent
377 errors.add :parent_issue_id, :not_a_valid_parent
371 end
378 end
372 end
379 end
373 end
380 end
374 end
381 end
375
382
376 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
383 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
377 # even if the user turns off the setting later
384 # even if the user turns off the setting later
378 def update_done_ratio_from_issue_status
385 def update_done_ratio_from_issue_status
379 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
386 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
380 self.done_ratio = status.default_done_ratio
387 self.done_ratio = status.default_done_ratio
381 end
388 end
382 end
389 end
383
390
384 def init_journal(user, notes = "")
391 def init_journal(user, notes = "")
385 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
392 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
386 @issue_before_change = self.clone
393 @issue_before_change = self.clone
387 @issue_before_change.status = self.status
394 @issue_before_change.status = self.status
388 @custom_values_before_change = {}
395 @custom_values_before_change = {}
389 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
396 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
390 # Make sure updated_on is updated when adding a note.
397 # Make sure updated_on is updated when adding a note.
391 updated_on_will_change!
398 updated_on_will_change!
392 @current_journal
399 @current_journal
393 end
400 end
394
401
395 # Return true if the issue is closed, otherwise false
402 # Return true if the issue is closed, otherwise false
396 def closed?
403 def closed?
397 self.status.is_closed?
404 self.status.is_closed?
398 end
405 end
399
406
400 # Return true if the issue is being reopened
407 # Return true if the issue is being reopened
401 def reopened?
408 def reopened?
402 if !new_record? && status_id_changed?
409 if !new_record? && status_id_changed?
403 status_was = IssueStatus.find_by_id(status_id_was)
410 status_was = IssueStatus.find_by_id(status_id_was)
404 status_new = IssueStatus.find_by_id(status_id)
411 status_new = IssueStatus.find_by_id(status_id)
405 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
412 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
406 return true
413 return true
407 end
414 end
408 end
415 end
409 false
416 false
410 end
417 end
411
418
412 # Return true if the issue is being closed
419 # Return true if the issue is being closed
413 def closing?
420 def closing?
414 if !new_record? && status_id_changed?
421 if !new_record? && status_id_changed?
415 status_was = IssueStatus.find_by_id(status_id_was)
422 status_was = IssueStatus.find_by_id(status_id_was)
416 status_new = IssueStatus.find_by_id(status_id)
423 status_new = IssueStatus.find_by_id(status_id)
417 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
424 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
418 return true
425 return true
419 end
426 end
420 end
427 end
421 false
428 false
422 end
429 end
423
430
424 # Returns true if the issue is overdue
431 # Returns true if the issue is overdue
425 def overdue?
432 def overdue?
426 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
433 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
427 end
434 end
428
435
429 # Is the amount of work done less than it should for the due date
436 # Is the amount of work done less than it should for the due date
430 def behind_schedule?
437 def behind_schedule?
431 return false if start_date.nil? || due_date.nil?
438 return false if start_date.nil? || due_date.nil?
432 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
439 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
433 return done_date <= Date.today
440 return done_date <= Date.today
434 end
441 end
435
442
436 # Does this issue have children?
443 # Does this issue have children?
437 def children?
444 def children?
438 !leaf?
445 !leaf?
439 end
446 end
440
447
441 # Users the issue can be assigned to
448 # Users the issue can be assigned to
442 def assignable_users
449 def assignable_users
443 users = project.assignable_users
450 users = project.assignable_users
444 users << author if author
451 users << author if author
445 users.uniq.sort
452 users.uniq.sort
446 end
453 end
447
454
448 # Versions that the issue can be assigned to
455 # Versions that the issue can be assigned to
449 def assignable_versions
456 def assignable_versions
450 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
457 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
451 end
458 end
452
459
453 # Returns true if this issue is blocked by another issue that is still open
460 # Returns true if this issue is blocked by another issue that is still open
454 def blocked?
461 def blocked?
455 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
462 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
456 end
463 end
457
464
458 # Returns an array of status that user is able to apply
465 # Returns an array of status that user is able to apply
459 def new_statuses_allowed_to(user, include_default=false)
466 def new_statuses_allowed_to(user, include_default=false)
460 statuses = status.find_new_statuses_allowed_to(
467 statuses = status.find_new_statuses_allowed_to(
461 user.roles_for_project(project),
468 user.roles_for_project(project),
462 tracker,
469 tracker,
463 author == user,
470 author == user,
464 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
471 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
465 )
472 )
466 statuses << status unless statuses.empty?
473 statuses << status unless statuses.empty?
467 statuses << IssueStatus.default if include_default
474 statuses << IssueStatus.default if include_default
468 statuses = statuses.uniq.sort
475 statuses = statuses.uniq.sort
469 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
476 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
470 end
477 end
471
478
472 # Returns the mail adresses of users that should be notified
479 # Returns the mail adresses of users that should be notified
473 def recipients
480 def recipients
474 notified = project.notified_users
481 notified = project.notified_users
475 # Author and assignee are always notified unless they have been
482 # Author and assignee are always notified unless they have been
476 # locked or don't want to be notified
483 # locked or don't want to be notified
477 notified << author if author && author.active? && author.notify_about?(self)
484 notified << author if author && author.active? && author.notify_about?(self)
478 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
485 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
479 notified.uniq!
486 notified.uniq!
480 # Remove users that can not view the issue
487 # Remove users that can not view the issue
481 notified.reject! {|user| !visible?(user)}
488 notified.reject! {|user| !visible?(user)}
482 notified.collect(&:mail)
489 notified.collect(&:mail)
483 end
490 end
484
491
485 # Returns the total number of hours spent on this issue and its descendants
492 # Returns the total number of hours spent on this issue and its descendants
486 #
493 #
487 # Example:
494 # Example:
488 # spent_hours => 0.0
495 # spent_hours => 0.0
489 # spent_hours => 50.2
496 # spent_hours => 50.2
490 def spent_hours
497 def spent_hours
491 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
498 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
492 end
499 end
493
500
494 def relations
501 def relations
495 (relations_from + relations_to).sort
502 (relations_from + relations_to).sort
496 end
503 end
497
504
498 def all_dependent_issues(except=[])
505 def all_dependent_issues(except=[])
499 except << self
506 except << self
500 dependencies = []
507 dependencies = []
501 relations_from.each do |relation|
508 relations_from.each do |relation|
502 if relation.issue_to && !except.include?(relation.issue_to)
509 if relation.issue_to && !except.include?(relation.issue_to)
503 dependencies << relation.issue_to
510 dependencies << relation.issue_to
504 dependencies += relation.issue_to.all_dependent_issues(except)
511 dependencies += relation.issue_to.all_dependent_issues(except)
505 end
512 end
506 end
513 end
507 dependencies
514 dependencies
508 end
515 end
509
516
510 # Returns an array of issues that duplicate this one
517 # Returns an array of issues that duplicate this one
511 def duplicates
518 def duplicates
512 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
519 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
513 end
520 end
514
521
515 # Returns the due date or the target due date if any
522 # Returns the due date or the target due date if any
516 # Used on gantt chart
523 # Used on gantt chart
517 def due_before
524 def due_before
518 due_date || (fixed_version ? fixed_version.effective_date : nil)
525 due_date || (fixed_version ? fixed_version.effective_date : nil)
519 end
526 end
520
527
521 # Returns the time scheduled for this issue.
528 # Returns the time scheduled for this issue.
522 #
529 #
523 # Example:
530 # Example:
524 # Start Date: 2/26/09, End Date: 3/04/09
531 # Start Date: 2/26/09, End Date: 3/04/09
525 # duration => 6
532 # duration => 6
526 def duration
533 def duration
527 (start_date && due_date) ? due_date - start_date : 0
534 (start_date && due_date) ? due_date - start_date : 0
528 end
535 end
529
536
530 def soonest_start
537 def soonest_start
531 @soonest_start ||= (
538 @soonest_start ||= (
532 relations_to.collect{|relation| relation.successor_soonest_start} +
539 relations_to.collect{|relation| relation.successor_soonest_start} +
533 ancestors.collect(&:soonest_start)
540 ancestors.collect(&:soonest_start)
534 ).compact.max
541 ).compact.max
535 end
542 end
536
543
537 def reschedule_after(date)
544 def reschedule_after(date)
538 return if date.nil?
545 return if date.nil?
539 if leaf?
546 if leaf?
540 if start_date.nil? || start_date < date
547 if start_date.nil? || start_date < date
541 self.start_date, self.due_date = date, date + duration
548 self.start_date, self.due_date = date, date + duration
542 save
549 save
543 end
550 end
544 else
551 else
545 leaves.each do |leaf|
552 leaves.each do |leaf|
546 leaf.reschedule_after(date)
553 leaf.reschedule_after(date)
547 end
554 end
548 end
555 end
549 end
556 end
550
557
551 def <=>(issue)
558 def <=>(issue)
552 if issue.nil?
559 if issue.nil?
553 -1
560 -1
554 elsif root_id != issue.root_id
561 elsif root_id != issue.root_id
555 (root_id || 0) <=> (issue.root_id || 0)
562 (root_id || 0) <=> (issue.root_id || 0)
556 else
563 else
557 (lft || 0) <=> (issue.lft || 0)
564 (lft || 0) <=> (issue.lft || 0)
558 end
565 end
559 end
566 end
560
567
561 def to_s
568 def to_s
562 "#{tracker} ##{id}: #{subject}"
569 "#{tracker} ##{id}: #{subject}"
563 end
570 end
564
571
565 # Returns a string of css classes that apply to the issue
572 # Returns a string of css classes that apply to the issue
566 def css_classes
573 def css_classes
567 s = "issue status-#{status.position} priority-#{priority.position}"
574 s = "issue status-#{status.position} priority-#{priority.position}"
568 s << ' closed' if closed?
575 s << ' closed' if closed?
569 s << ' overdue' if overdue?
576 s << ' overdue' if overdue?
570 s << ' child' if child?
577 s << ' child' if child?
571 s << ' parent' unless leaf?
578 s << ' parent' unless leaf?
572 s << ' private' if is_private?
579 s << ' private' if is_private?
573 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
580 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
574 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
581 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
575 s
582 s
576 end
583 end
577
584
578 # Saves an issue, time_entry, attachments, and a journal from the parameters
585 # Saves an issue, time_entry, attachments, and a journal from the parameters
579 # Returns false if save fails
586 # Returns false if save fails
580 def save_issue_with_child_records(params, existing_time_entry=nil)
587 def save_issue_with_child_records(params, existing_time_entry=nil)
581 Issue.transaction do
588 Issue.transaction do
582 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
589 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
583 @time_entry = existing_time_entry || TimeEntry.new
590 @time_entry = existing_time_entry || TimeEntry.new
584 @time_entry.project = project
591 @time_entry.project = project
585 @time_entry.issue = self
592 @time_entry.issue = self
586 @time_entry.user = User.current
593 @time_entry.user = User.current
587 @time_entry.spent_on = Date.today
594 @time_entry.spent_on = Date.today
588 @time_entry.attributes = params[:time_entry]
595 @time_entry.attributes = params[:time_entry]
589 self.time_entries << @time_entry
596 self.time_entries << @time_entry
590 end
597 end
591
598
592 if valid?
599 if valid?
593 attachments = Attachment.attach_files(self, params[:attachments])
600 attachments = Attachment.attach_files(self, params[:attachments])
594
601
595 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
602 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
596 # TODO: Rename hook
603 # TODO: Rename hook
597 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
604 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
598 begin
605 begin
599 if save
606 if save
600 # TODO: Rename hook
607 # TODO: Rename hook
601 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
608 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
602 else
609 else
603 raise ActiveRecord::Rollback
610 raise ActiveRecord::Rollback
604 end
611 end
605 rescue ActiveRecord::StaleObjectError
612 rescue ActiveRecord::StaleObjectError
606 attachments[:files].each(&:destroy)
613 attachments[:files].each(&:destroy)
607 errors.add_to_base l(:notice_locking_conflict)
614 errors.add_to_base l(:notice_locking_conflict)
608 raise ActiveRecord::Rollback
615 raise ActiveRecord::Rollback
609 end
616 end
610 end
617 end
611 end
618 end
612 end
619 end
613
620
614 # Unassigns issues from +version+ if it's no longer shared with issue's project
621 # Unassigns issues from +version+ if it's no longer shared with issue's project
615 def self.update_versions_from_sharing_change(version)
622 def self.update_versions_from_sharing_change(version)
616 # Update issues assigned to the version
623 # Update issues assigned to the version
617 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
624 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
618 end
625 end
619
626
620 # Unassigns issues from versions that are no longer shared
627 # Unassigns issues from versions that are no longer shared
621 # after +project+ was moved
628 # after +project+ was moved
622 def self.update_versions_from_hierarchy_change(project)
629 def self.update_versions_from_hierarchy_change(project)
623 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
630 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
624 # Update issues of the moved projects and issues assigned to a version of a moved project
631 # Update issues of the moved projects and issues assigned to a version of a moved project
625 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
632 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
626 end
633 end
627
634
628 def parent_issue_id=(arg)
635 def parent_issue_id=(arg)
629 parent_issue_id = arg.blank? ? nil : arg.to_i
636 parent_issue_id = arg.blank? ? nil : arg.to_i
630 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
637 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
631 @parent_issue.id
638 @parent_issue.id
632 else
639 else
633 @parent_issue = nil
640 @parent_issue = nil
634 nil
641 nil
635 end
642 end
636 end
643 end
637
644
638 def parent_issue_id
645 def parent_issue_id
639 if instance_variable_defined? :@parent_issue
646 if instance_variable_defined? :@parent_issue
640 @parent_issue.nil? ? nil : @parent_issue.id
647 @parent_issue.nil? ? nil : @parent_issue.id
641 else
648 else
642 parent_id
649 parent_id
643 end
650 end
644 end
651 end
645
652
646 # Extracted from the ReportsController.
653 # Extracted from the ReportsController.
647 def self.by_tracker(project)
654 def self.by_tracker(project)
648 count_and_group_by(:project => project,
655 count_and_group_by(:project => project,
649 :field => 'tracker_id',
656 :field => 'tracker_id',
650 :joins => Tracker.table_name)
657 :joins => Tracker.table_name)
651 end
658 end
652
659
653 def self.by_version(project)
660 def self.by_version(project)
654 count_and_group_by(:project => project,
661 count_and_group_by(:project => project,
655 :field => 'fixed_version_id',
662 :field => 'fixed_version_id',
656 :joins => Version.table_name)
663 :joins => Version.table_name)
657 end
664 end
658
665
659 def self.by_priority(project)
666 def self.by_priority(project)
660 count_and_group_by(:project => project,
667 count_and_group_by(:project => project,
661 :field => 'priority_id',
668 :field => 'priority_id',
662 :joins => IssuePriority.table_name)
669 :joins => IssuePriority.table_name)
663 end
670 end
664
671
665 def self.by_category(project)
672 def self.by_category(project)
666 count_and_group_by(:project => project,
673 count_and_group_by(:project => project,
667 :field => 'category_id',
674 :field => 'category_id',
668 :joins => IssueCategory.table_name)
675 :joins => IssueCategory.table_name)
669 end
676 end
670
677
671 def self.by_assigned_to(project)
678 def self.by_assigned_to(project)
672 count_and_group_by(:project => project,
679 count_and_group_by(:project => project,
673 :field => 'assigned_to_id',
680 :field => 'assigned_to_id',
674 :joins => User.table_name)
681 :joins => User.table_name)
675 end
682 end
676
683
677 def self.by_author(project)
684 def self.by_author(project)
678 count_and_group_by(:project => project,
685 count_and_group_by(:project => project,
679 :field => 'author_id',
686 :field => 'author_id',
680 :joins => User.table_name)
687 :joins => User.table_name)
681 end
688 end
682
689
683 def self.by_subproject(project)
690 def self.by_subproject(project)
684 ActiveRecord::Base.connection.select_all("select s.id as status_id,
691 ActiveRecord::Base.connection.select_all("select s.id as status_id,
685 s.is_closed as closed,
692 s.is_closed as closed,
686 #{Issue.table_name}.project_id as project_id,
693 #{Issue.table_name}.project_id as project_id,
687 count(#{Issue.table_name}.id) as total
694 count(#{Issue.table_name}.id) as total
688 from
695 from
689 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
696 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
690 where
697 where
691 #{Issue.table_name}.status_id=s.id
698 #{Issue.table_name}.status_id=s.id
692 and #{Issue.table_name}.project_id = #{Project.table_name}.id
699 and #{Issue.table_name}.project_id = #{Project.table_name}.id
693 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
700 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
694 and #{Issue.table_name}.project_id <> #{project.id}
701 and #{Issue.table_name}.project_id <> #{project.id}
695 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
702 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
696 end
703 end
697 # End ReportsController extraction
704 # End ReportsController extraction
698
705
699 # Returns an array of projects that current user can move issues to
706 # Returns an array of projects that current user can move issues to
700 def self.allowed_target_projects_on_move
707 def self.allowed_target_projects_on_move
701 projects = []
708 projects = []
702 if User.current.admin?
709 if User.current.admin?
703 # admin is allowed to move issues to any active (visible) project
710 # admin is allowed to move issues to any active (visible) project
704 projects = Project.visible.all
711 projects = Project.visible.all
705 elsif User.current.logged?
712 elsif User.current.logged?
706 if Role.non_member.allowed_to?(:move_issues)
713 if Role.non_member.allowed_to?(:move_issues)
707 projects = Project.visible.all
714 projects = Project.visible.all
708 else
715 else
709 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
716 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
710 end
717 end
711 end
718 end
712 projects
719 projects
713 end
720 end
714
721
715 private
722 private
716
723
717 def update_nested_set_attributes
724 def update_nested_set_attributes
718 if root_id.nil?
725 if root_id.nil?
719 # issue was just created
726 # issue was just created
720 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
727 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
721 set_default_left_and_right
728 set_default_left_and_right
722 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
729 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
723 if @parent_issue
730 if @parent_issue
724 move_to_child_of(@parent_issue)
731 move_to_child_of(@parent_issue)
725 end
732 end
726 reload
733 reload
727 elsif parent_issue_id != parent_id
734 elsif parent_issue_id != parent_id
728 former_parent_id = parent_id
735 former_parent_id = parent_id
729 # moving an existing issue
736 # moving an existing issue
730 if @parent_issue && @parent_issue.root_id == root_id
737 if @parent_issue && @parent_issue.root_id == root_id
731 # inside the same tree
738 # inside the same tree
732 move_to_child_of(@parent_issue)
739 move_to_child_of(@parent_issue)
733 else
740 else
734 # to another tree
741 # to another tree
735 unless root?
742 unless root?
736 move_to_right_of(root)
743 move_to_right_of(root)
737 reload
744 reload
738 end
745 end
739 old_root_id = root_id
746 old_root_id = root_id
740 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
747 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
741 target_maxright = nested_set_scope.maximum(right_column_name) || 0
748 target_maxright = nested_set_scope.maximum(right_column_name) || 0
742 offset = target_maxright + 1 - lft
749 offset = target_maxright + 1 - lft
743 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
750 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
744 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
751 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
745 self[left_column_name] = lft + offset
752 self[left_column_name] = lft + offset
746 self[right_column_name] = rgt + offset
753 self[right_column_name] = rgt + offset
747 if @parent_issue
754 if @parent_issue
748 move_to_child_of(@parent_issue)
755 move_to_child_of(@parent_issue)
749 end
756 end
750 end
757 end
751 reload
758 reload
752 # delete invalid relations of all descendants
759 # delete invalid relations of all descendants
753 self_and_descendants.each do |issue|
760 self_and_descendants.each do |issue|
754 issue.relations.each do |relation|
761 issue.relations.each do |relation|
755 relation.destroy unless relation.valid?
762 relation.destroy unless relation.valid?
756 end
763 end
757 end
764 end
758 # update former parent
765 # update former parent
759 recalculate_attributes_for(former_parent_id) if former_parent_id
766 recalculate_attributes_for(former_parent_id) if former_parent_id
760 end
767 end
761 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
768 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
762 end
769 end
763
770
764 def update_parent_attributes
771 def update_parent_attributes
765 recalculate_attributes_for(parent_id) if parent_id
772 recalculate_attributes_for(parent_id) if parent_id
766 end
773 end
767
774
768 def recalculate_attributes_for(issue_id)
775 def recalculate_attributes_for(issue_id)
769 if issue_id && p = Issue.find_by_id(issue_id)
776 if issue_id && p = Issue.find_by_id(issue_id)
770 # priority = highest priority of children
777 # priority = highest priority of children
771 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
778 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
772 p.priority = IssuePriority.find_by_position(priority_position)
779 p.priority = IssuePriority.find_by_position(priority_position)
773 end
780 end
774
781
775 # start/due dates = lowest/highest dates of children
782 # start/due dates = lowest/highest dates of children
776 p.start_date = p.children.minimum(:start_date)
783 p.start_date = p.children.minimum(:start_date)
777 p.due_date = p.children.maximum(:due_date)
784 p.due_date = p.children.maximum(:due_date)
778 if p.start_date && p.due_date && p.due_date < p.start_date
785 if p.start_date && p.due_date && p.due_date < p.start_date
779 p.start_date, p.due_date = p.due_date, p.start_date
786 p.start_date, p.due_date = p.due_date, p.start_date
780 end
787 end
781
788
782 # done ratio = weighted average ratio of leaves
789 # done ratio = weighted average ratio of leaves
783 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
790 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
784 leaves_count = p.leaves.count
791 leaves_count = p.leaves.count
785 if leaves_count > 0
792 if leaves_count > 0
786 average = p.leaves.average(:estimated_hours).to_f
793 average = p.leaves.average(:estimated_hours).to_f
787 if average == 0
794 if average == 0
788 average = 1
795 average = 1
789 end
796 end
790 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
797 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
791 progress = done / (average * leaves_count)
798 progress = done / (average * leaves_count)
792 p.done_ratio = progress.round
799 p.done_ratio = progress.round
793 end
800 end
794 end
801 end
795
802
796 # estimate = sum of leaves estimates
803 # estimate = sum of leaves estimates
797 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
804 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
798 p.estimated_hours = nil if p.estimated_hours == 0.0
805 p.estimated_hours = nil if p.estimated_hours == 0.0
799
806
800 # ancestors will be recursively updated
807 # ancestors will be recursively updated
801 p.save(false)
808 p.save(false)
802 end
809 end
803 end
810 end
804
811
805 # Update issues so their versions are not pointing to a
812 # Update issues so their versions are not pointing to a
806 # fixed_version that is not shared with the issue's project
813 # fixed_version that is not shared with the issue's project
807 def self.update_versions(conditions=nil)
814 def self.update_versions(conditions=nil)
808 # Only need to update issues with a fixed_version from
815 # Only need to update issues with a fixed_version from
809 # a different project and that is not systemwide shared
816 # a different project and that is not systemwide shared
810 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
817 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
811 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
818 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
812 " AND #{Version.table_name}.sharing <> 'system'",
819 " AND #{Version.table_name}.sharing <> 'system'",
813 conditions),
820 conditions),
814 :include => [:project, :fixed_version]
821 :include => [:project, :fixed_version]
815 ).each do |issue|
822 ).each do |issue|
816 next if issue.project.nil? || issue.fixed_version.nil?
823 next if issue.project.nil? || issue.fixed_version.nil?
817 unless issue.project.shared_versions.include?(issue.fixed_version)
824 unless issue.project.shared_versions.include?(issue.fixed_version)
818 issue.init_journal(User.current)
825 issue.init_journal(User.current)
819 issue.fixed_version = nil
826 issue.fixed_version = nil
820 issue.save
827 issue.save
821 end
828 end
822 end
829 end
823 end
830 end
824
831
825 # Callback on attachment deletion
832 # Callback on attachment deletion
826 def attachment_removed(obj)
833 def attachment_removed(obj)
827 journal = init_journal(User.current)
834 journal = init_journal(User.current)
828 journal.details << JournalDetail.new(:property => 'attachment',
835 journal.details << JournalDetail.new(:property => 'attachment',
829 :prop_key => obj.id,
836 :prop_key => obj.id,
830 :old_value => obj.filename)
837 :old_value => obj.filename)
831 journal.save
838 journal.save
832 end
839 end
833
840
834 # Default assignment based on category
841 # Default assignment based on category
835 def default_assign
842 def default_assign
836 if assigned_to.nil? && category && category.assigned_to
843 if assigned_to.nil? && category && category.assigned_to
837 self.assigned_to = category.assigned_to
844 self.assigned_to = category.assigned_to
838 end
845 end
839 end
846 end
840
847
841 # Updates start/due dates of following issues
848 # Updates start/due dates of following issues
842 def reschedule_following_issues
849 def reschedule_following_issues
843 if start_date_changed? || due_date_changed?
850 if start_date_changed? || due_date_changed?
844 relations_from.each do |relation|
851 relations_from.each do |relation|
845 relation.set_issue_to_dates
852 relation.set_issue_to_dates
846 end
853 end
847 end
854 end
848 end
855 end
849
856
850 # Closes duplicates if the issue is being closed
857 # Closes duplicates if the issue is being closed
851 def close_duplicates
858 def close_duplicates
852 if closing?
859 if closing?
853 duplicates.each do |duplicate|
860 duplicates.each do |duplicate|
854 # Reload is need in case the duplicate was updated by a previous duplicate
861 # Reload is need in case the duplicate was updated by a previous duplicate
855 duplicate.reload
862 duplicate.reload
856 # Don't re-close it if it's already closed
863 # Don't re-close it if it's already closed
857 next if duplicate.closed?
864 next if duplicate.closed?
858 # Same user and notes
865 # Same user and notes
859 if @current_journal
866 if @current_journal
860 duplicate.init_journal(@current_journal.user, @current_journal.notes)
867 duplicate.init_journal(@current_journal.user, @current_journal.notes)
861 end
868 end
862 duplicate.update_attribute :status, self.status
869 duplicate.update_attribute :status, self.status
863 end
870 end
864 end
871 end
865 end
872 end
866
873
867 # Saves the changes in a Journal
874 # Saves the changes in a Journal
868 # Called after_save
875 # Called after_save
869 def create_journal
876 def create_journal
870 if @current_journal
877 if @current_journal
871 # attributes changes
878 # attributes changes
872 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
879 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
880 before = @issue_before_change.send(c)
881 after = send(c)
882 next if before == after || (before.blank? && after.blank?)
873 @current_journal.details << JournalDetail.new(:property => 'attr',
883 @current_journal.details << JournalDetail.new(:property => 'attr',
874 :prop_key => c,
884 :prop_key => c,
875 :old_value => @issue_before_change.send(c),
885 :old_value => @issue_before_change.send(c),
876 :value => send(c)) unless send(c)==@issue_before_change.send(c)
886 :value => send(c))
877 }
887 }
878 # custom fields changes
888 # custom fields changes
879 custom_values.each {|c|
889 custom_values.each {|c|
880 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
890 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
881 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
891 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
882 @current_journal.details << JournalDetail.new(:property => 'cf',
892 @current_journal.details << JournalDetail.new(:property => 'cf',
883 :prop_key => c.custom_field_id,
893 :prop_key => c.custom_field_id,
884 :old_value => @custom_values_before_change[c.custom_field_id],
894 :old_value => @custom_values_before_change[c.custom_field_id],
885 :value => c.value)
895 :value => c.value)
886 }
896 }
887 @current_journal.save
897 @current_journal.save
888 # reset current journal
898 # reset current journal
889 init_journal @current_journal.user, @current_journal.notes
899 init_journal @current_journal.user, @current_journal.notes
890 end
900 end
891 end
901 end
892
902
893 # Query generator for selecting groups of issue counts for a project
903 # Query generator for selecting groups of issue counts for a project
894 # based on specific criteria
904 # based on specific criteria
895 #
905 #
896 # Options
906 # Options
897 # * project - Project to search in.
907 # * project - Project to search in.
898 # * field - String. Issue field to key off of in the grouping.
908 # * field - String. Issue field to key off of in the grouping.
899 # * joins - String. The table name to join against.
909 # * joins - String. The table name to join against.
900 def self.count_and_group_by(options)
910 def self.count_and_group_by(options)
901 project = options.delete(:project)
911 project = options.delete(:project)
902 select_field = options.delete(:field)
912 select_field = options.delete(:field)
903 joins = options.delete(:joins)
913 joins = options.delete(:joins)
904
914
905 where = "#{Issue.table_name}.#{select_field}=j.id"
915 where = "#{Issue.table_name}.#{select_field}=j.id"
906
916
907 ActiveRecord::Base.connection.select_all("select s.id as status_id,
917 ActiveRecord::Base.connection.select_all("select s.id as status_id,
908 s.is_closed as closed,
918 s.is_closed as closed,
909 j.id as #{select_field},
919 j.id as #{select_field},
910 count(#{Issue.table_name}.id) as total
920 count(#{Issue.table_name}.id) as total
911 from
921 from
912 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
922 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
913 where
923 where
914 #{Issue.table_name}.status_id=s.id
924 #{Issue.table_name}.status_id=s.id
915 and #{where}
925 and #{where}
916 and #{Issue.table_name}.project_id=#{Project.table_name}.id
926 and #{Issue.table_name}.project_id=#{Project.table_name}.id
917 and #{visible_condition(User.current, :project => project)}
927 and #{visible_condition(User.current, :project => project)}
918 group by s.id, s.is_closed, j.id")
928 group by s.id, s.is_closed, j.id")
919 end
929 end
920 end
930 end
@@ -1,994 +1,1015
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :enabled_modules,
23 :enabled_modules,
24 :versions,
24 :versions,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :enumerations,
26 :enumerations,
27 :issues,
27 :issues,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :time_entries
29 :time_entries
30
30
31 def test_create
31 def test_create
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
33 assert issue.save
33 assert issue.save
34 issue.reload
34 issue.reload
35 assert_equal 1.5, issue.estimated_hours
35 assert_equal 1.5, issue.estimated_hours
36 end
36 end
37
37
38 def test_create_minimal
38 def test_create_minimal
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 assert issue.save
40 assert issue.save
41 assert issue.description.nil?
41 assert issue.description.nil?
42 end
42 end
43
43
44 def test_create_with_required_custom_field
44 def test_create_with_required_custom_field
45 field = IssueCustomField.find_by_name('Database')
45 field = IssueCustomField.find_by_name('Database')
46 field.update_attribute(:is_required, true)
46 field.update_attribute(:is_required, true)
47
47
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
49 assert issue.available_custom_fields.include?(field)
49 assert issue.available_custom_fields.include?(field)
50 # No value for the custom field
50 # No value for the custom field
51 assert !issue.save
51 assert !issue.save
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 # Blank value
53 # Blank value
54 issue.custom_field_values = { field.id => '' }
54 issue.custom_field_values = { field.id => '' }
55 assert !issue.save
55 assert !issue.save
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 # Invalid value
57 # Invalid value
58 issue.custom_field_values = { field.id => 'SQLServer' }
58 issue.custom_field_values = { field.id => 'SQLServer' }
59 assert !issue.save
59 assert !issue.save
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 # Valid value
61 # Valid value
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 assert issue.save
63 assert issue.save
64 issue.reload
64 issue.reload
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 end
66 end
67
67
68 def assert_visibility_match(user, issues)
68 def assert_visibility_match(user, issues)
69 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
69 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
70 end
70 end
71
71
72 def test_visible_scope_for_anonymous
72 def test_visible_scope_for_anonymous
73 # Anonymous user should see issues of public projects only
73 # Anonymous user should see issues of public projects only
74 issues = Issue.visible(User.anonymous).all
74 issues = Issue.visible(User.anonymous).all
75 assert issues.any?
75 assert issues.any?
76 assert_nil issues.detect {|issue| !issue.project.is_public?}
76 assert_nil issues.detect {|issue| !issue.project.is_public?}
77 assert_nil issues.detect {|issue| issue.is_private?}
77 assert_nil issues.detect {|issue| issue.is_private?}
78 assert_visibility_match User.anonymous, issues
78 assert_visibility_match User.anonymous, issues
79 end
79 end
80
80
81 def test_visible_scope_for_anonymous_with_own_issues_visibility
81 def test_visible_scope_for_anonymous_with_own_issues_visibility
82 Role.anonymous.update_attribute :issues_visibility, 'own'
82 Role.anonymous.update_attribute :issues_visibility, 'own'
83 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => User.anonymous.id, :subject => 'Issue by anonymous')
83 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => User.anonymous.id, :subject => 'Issue by anonymous')
84
84
85 issues = Issue.visible(User.anonymous).all
85 issues = Issue.visible(User.anonymous).all
86 assert issues.any?
86 assert issues.any?
87 assert_nil issues.detect {|issue| issue.author != User.anonymous}
87 assert_nil issues.detect {|issue| issue.author != User.anonymous}
88 assert_visibility_match User.anonymous, issues
88 assert_visibility_match User.anonymous, issues
89 end
89 end
90
90
91 def test_visible_scope_for_anonymous_without_view_issues_permissions
91 def test_visible_scope_for_anonymous_without_view_issues_permissions
92 # Anonymous user should not see issues without permission
92 # Anonymous user should not see issues without permission
93 Role.anonymous.remove_permission!(:view_issues)
93 Role.anonymous.remove_permission!(:view_issues)
94 issues = Issue.visible(User.anonymous).all
94 issues = Issue.visible(User.anonymous).all
95 assert issues.empty?
95 assert issues.empty?
96 assert_visibility_match User.anonymous, issues
96 assert_visibility_match User.anonymous, issues
97 end
97 end
98
98
99 def test_visible_scope_for_non_member
99 def test_visible_scope_for_non_member
100 user = User.find(9)
100 user = User.find(9)
101 assert user.projects.empty?
101 assert user.projects.empty?
102 # Non member user should see issues of public projects only
102 # Non member user should see issues of public projects only
103 issues = Issue.visible(user).all
103 issues = Issue.visible(user).all
104 assert issues.any?
104 assert issues.any?
105 assert_nil issues.detect {|issue| !issue.project.is_public?}
105 assert_nil issues.detect {|issue| !issue.project.is_public?}
106 assert_nil issues.detect {|issue| issue.is_private?}
106 assert_nil issues.detect {|issue| issue.is_private?}
107 assert_visibility_match user, issues
107 assert_visibility_match user, issues
108 end
108 end
109
109
110 def test_visible_scope_for_non_member_with_own_issues_visibility
110 def test_visible_scope_for_non_member_with_own_issues_visibility
111 Role.non_member.update_attribute :issues_visibility, 'own'
111 Role.non_member.update_attribute :issues_visibility, 'own'
112 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
112 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
113 user = User.find(9)
113 user = User.find(9)
114
114
115 issues = Issue.visible(user).all
115 issues = Issue.visible(user).all
116 assert issues.any?
116 assert issues.any?
117 assert_nil issues.detect {|issue| issue.author != user}
117 assert_nil issues.detect {|issue| issue.author != user}
118 assert_visibility_match user, issues
118 assert_visibility_match user, issues
119 end
119 end
120
120
121 def test_visible_scope_for_non_member_without_view_issues_permissions
121 def test_visible_scope_for_non_member_without_view_issues_permissions
122 # Non member user should not see issues without permission
122 # Non member user should not see issues without permission
123 Role.non_member.remove_permission!(:view_issues)
123 Role.non_member.remove_permission!(:view_issues)
124 user = User.find(9)
124 user = User.find(9)
125 assert user.projects.empty?
125 assert user.projects.empty?
126 issues = Issue.visible(user).all
126 issues = Issue.visible(user).all
127 assert issues.empty?
127 assert issues.empty?
128 assert_visibility_match user, issues
128 assert_visibility_match user, issues
129 end
129 end
130
130
131 def test_visible_scope_for_member
131 def test_visible_scope_for_member
132 user = User.find(9)
132 user = User.find(9)
133 # User should see issues of projects for which he has view_issues permissions only
133 # User should see issues of projects for which he has view_issues permissions only
134 Role.non_member.remove_permission!(:view_issues)
134 Role.non_member.remove_permission!(:view_issues)
135 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
135 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
136 issues = Issue.visible(user).all
136 issues = Issue.visible(user).all
137 assert issues.any?
137 assert issues.any?
138 assert_nil issues.detect {|issue| issue.project_id != 3}
138 assert_nil issues.detect {|issue| issue.project_id != 3}
139 assert_nil issues.detect {|issue| issue.is_private?}
139 assert_nil issues.detect {|issue| issue.is_private?}
140 assert_visibility_match user, issues
140 assert_visibility_match user, issues
141 end
141 end
142
142
143 def test_visible_scope_for_admin
143 def test_visible_scope_for_admin
144 user = User.find(1)
144 user = User.find(1)
145 user.members.each(&:destroy)
145 user.members.each(&:destroy)
146 assert user.projects.empty?
146 assert user.projects.empty?
147 issues = Issue.visible(user).all
147 issues = Issue.visible(user).all
148 assert issues.any?
148 assert issues.any?
149 # Admin should see issues on private projects that he does not belong to
149 # Admin should see issues on private projects that he does not belong to
150 assert issues.detect {|issue| !issue.project.is_public?}
150 assert issues.detect {|issue| !issue.project.is_public?}
151 # Admin should see private issues of other users
151 # Admin should see private issues of other users
152 assert issues.detect {|issue| issue.is_private? && issue.author != user}
152 assert issues.detect {|issue| issue.is_private? && issue.author != user}
153 assert_visibility_match user, issues
153 assert_visibility_match user, issues
154 end
154 end
155
155
156 def test_visible_scope_with_project
156 def test_visible_scope_with_project
157 project = Project.find(1)
157 project = Project.find(1)
158 issues = Issue.visible(User.find(2), :project => project).all
158 issues = Issue.visible(User.find(2), :project => project).all
159 projects = issues.collect(&:project).uniq
159 projects = issues.collect(&:project).uniq
160 assert_equal 1, projects.size
160 assert_equal 1, projects.size
161 assert_equal project, projects.first
161 assert_equal project, projects.first
162 end
162 end
163
163
164 def test_visible_scope_with_project_and_subprojects
164 def test_visible_scope_with_project_and_subprojects
165 project = Project.find(1)
165 project = Project.find(1)
166 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
166 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
167 projects = issues.collect(&:project).uniq
167 projects = issues.collect(&:project).uniq
168 assert projects.size > 1
168 assert projects.size > 1
169 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
169 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
170 end
170 end
171
171
172 def test_visible_and_nested_set_scopes
172 def test_visible_and_nested_set_scopes
173 assert_equal 0, Issue.find(1).descendants.visible.all.size
173 assert_equal 0, Issue.find(1).descendants.visible.all.size
174 end
174 end
175
175
176 def test_errors_full_messages_should_include_custom_fields_errors
176 def test_errors_full_messages_should_include_custom_fields_errors
177 field = IssueCustomField.find_by_name('Database')
177 field = IssueCustomField.find_by_name('Database')
178
178
179 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
179 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
180 assert issue.available_custom_fields.include?(field)
180 assert issue.available_custom_fields.include?(field)
181 # Invalid value
181 # Invalid value
182 issue.custom_field_values = { field.id => 'SQLServer' }
182 issue.custom_field_values = { field.id => 'SQLServer' }
183
183
184 assert !issue.valid?
184 assert !issue.valid?
185 assert_equal 1, issue.errors.full_messages.size
185 assert_equal 1, issue.errors.full_messages.size
186 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
186 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
187 end
187 end
188
188
189 def test_update_issue_with_required_custom_field
189 def test_update_issue_with_required_custom_field
190 field = IssueCustomField.find_by_name('Database')
190 field = IssueCustomField.find_by_name('Database')
191 field.update_attribute(:is_required, true)
191 field.update_attribute(:is_required, true)
192
192
193 issue = Issue.find(1)
193 issue = Issue.find(1)
194 assert_nil issue.custom_value_for(field)
194 assert_nil issue.custom_value_for(field)
195 assert issue.available_custom_fields.include?(field)
195 assert issue.available_custom_fields.include?(field)
196 # No change to custom values, issue can be saved
196 # No change to custom values, issue can be saved
197 assert issue.save
197 assert issue.save
198 # Blank value
198 # Blank value
199 issue.custom_field_values = { field.id => '' }
199 issue.custom_field_values = { field.id => '' }
200 assert !issue.save
200 assert !issue.save
201 # Valid value
201 # Valid value
202 issue.custom_field_values = { field.id => 'PostgreSQL' }
202 issue.custom_field_values = { field.id => 'PostgreSQL' }
203 assert issue.save
203 assert issue.save
204 issue.reload
204 issue.reload
205 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
205 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
206 end
206 end
207
207
208 def test_should_not_update_attributes_if_custom_fields_validation_fails
208 def test_should_not_update_attributes_if_custom_fields_validation_fails
209 issue = Issue.find(1)
209 issue = Issue.find(1)
210 field = IssueCustomField.find_by_name('Database')
210 field = IssueCustomField.find_by_name('Database')
211 assert issue.available_custom_fields.include?(field)
211 assert issue.available_custom_fields.include?(field)
212
212
213 issue.custom_field_values = { field.id => 'Invalid' }
213 issue.custom_field_values = { field.id => 'Invalid' }
214 issue.subject = 'Should be not be saved'
214 issue.subject = 'Should be not be saved'
215 assert !issue.save
215 assert !issue.save
216
216
217 issue.reload
217 issue.reload
218 assert_equal "Can't print recipes", issue.subject
218 assert_equal "Can't print recipes", issue.subject
219 end
219 end
220
220
221 def test_should_not_recreate_custom_values_objects_on_update
221 def test_should_not_recreate_custom_values_objects_on_update
222 field = IssueCustomField.find_by_name('Database')
222 field = IssueCustomField.find_by_name('Database')
223
223
224 issue = Issue.find(1)
224 issue = Issue.find(1)
225 issue.custom_field_values = { field.id => 'PostgreSQL' }
225 issue.custom_field_values = { field.id => 'PostgreSQL' }
226 assert issue.save
226 assert issue.save
227 custom_value = issue.custom_value_for(field)
227 custom_value = issue.custom_value_for(field)
228 issue.reload
228 issue.reload
229 issue.custom_field_values = { field.id => 'MySQL' }
229 issue.custom_field_values = { field.id => 'MySQL' }
230 assert issue.save
230 assert issue.save
231 issue.reload
231 issue.reload
232 assert_equal custom_value.id, issue.custom_value_for(field).id
232 assert_equal custom_value.id, issue.custom_value_for(field).id
233 end
233 end
234
234
235 def test_assigning_tracker_id_should_reload_custom_fields_values
235 def test_assigning_tracker_id_should_reload_custom_fields_values
236 issue = Issue.new(:project => Project.find(1))
236 issue = Issue.new(:project => Project.find(1))
237 assert issue.custom_field_values.empty?
237 assert issue.custom_field_values.empty?
238 issue.tracker_id = 1
238 issue.tracker_id = 1
239 assert issue.custom_field_values.any?
239 assert issue.custom_field_values.any?
240 end
240 end
241
241
242 def test_assigning_attributes_should_assign_tracker_id_first
242 def test_assigning_attributes_should_assign_tracker_id_first
243 attributes = ActiveSupport::OrderedHash.new
243 attributes = ActiveSupport::OrderedHash.new
244 attributes['custom_field_values'] = { '1' => 'MySQL' }
244 attributes['custom_field_values'] = { '1' => 'MySQL' }
245 attributes['tracker_id'] = '1'
245 attributes['tracker_id'] = '1'
246 issue = Issue.new(:project => Project.find(1))
246 issue = Issue.new(:project => Project.find(1))
247 issue.attributes = attributes
247 issue.attributes = attributes
248 assert_not_nil issue.custom_value_for(1)
248 assert_not_nil issue.custom_value_for(1)
249 assert_equal 'MySQL', issue.custom_value_for(1).value
249 assert_equal 'MySQL', issue.custom_value_for(1).value
250 end
250 end
251
251
252 def test_should_update_issue_with_disabled_tracker
252 def test_should_update_issue_with_disabled_tracker
253 p = Project.find(1)
253 p = Project.find(1)
254 issue = Issue.find(1)
254 issue = Issue.find(1)
255
255
256 p.trackers.delete(issue.tracker)
256 p.trackers.delete(issue.tracker)
257 assert !p.trackers.include?(issue.tracker)
257 assert !p.trackers.include?(issue.tracker)
258
258
259 issue.reload
259 issue.reload
260 issue.subject = 'New subject'
260 issue.subject = 'New subject'
261 assert issue.save
261 assert issue.save
262 end
262 end
263
263
264 def test_should_not_set_a_disabled_tracker
264 def test_should_not_set_a_disabled_tracker
265 p = Project.find(1)
265 p = Project.find(1)
266 p.trackers.delete(Tracker.find(2))
266 p.trackers.delete(Tracker.find(2))
267
267
268 issue = Issue.find(1)
268 issue = Issue.find(1)
269 issue.tracker_id = 2
269 issue.tracker_id = 2
270 issue.subject = 'New subject'
270 issue.subject = 'New subject'
271 assert !issue.save
271 assert !issue.save
272 assert_not_nil issue.errors.on(:tracker_id)
272 assert_not_nil issue.errors.on(:tracker_id)
273 end
273 end
274
274
275 def test_category_based_assignment
275 def test_category_based_assignment
276 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
276 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
277 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
277 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
278 end
278 end
279
279
280 def test_new_statuses_allowed_to
280 def test_new_statuses_allowed_to
281 Workflow.delete_all
281 Workflow.delete_all
282
282
283 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
283 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
284 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
284 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
285 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
285 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
286 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
286 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
287 status = IssueStatus.find(1)
287 status = IssueStatus.find(1)
288 role = Role.find(1)
288 role = Role.find(1)
289 tracker = Tracker.find(1)
289 tracker = Tracker.find(1)
290 user = User.find(2)
290 user = User.find(2)
291
291
292 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
292 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
293 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
293 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
294
294
295 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
295 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
296 assert_equal [1, 2, 3], issue.new_statuses_allowed_to(user).map(&:id)
296 assert_equal [1, 2, 3], issue.new_statuses_allowed_to(user).map(&:id)
297
297
298 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
298 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
299 assert_equal [1, 2, 4], issue.new_statuses_allowed_to(user).map(&:id)
299 assert_equal [1, 2, 4], issue.new_statuses_allowed_to(user).map(&:id)
300
300
301 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
301 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
302 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
302 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
303 end
303 end
304
304
305 def test_copy
305 def test_copy
306 issue = Issue.new.copy_from(1)
306 issue = Issue.new.copy_from(1)
307 assert issue.save
307 assert issue.save
308 issue.reload
308 issue.reload
309 orig = Issue.find(1)
309 orig = Issue.find(1)
310 assert_equal orig.subject, issue.subject
310 assert_equal orig.subject, issue.subject
311 assert_equal orig.tracker, issue.tracker
311 assert_equal orig.tracker, issue.tracker
312 assert_equal "125", issue.custom_value_for(2).value
312 assert_equal "125", issue.custom_value_for(2).value
313 end
313 end
314
314
315 def test_copy_should_copy_status
315 def test_copy_should_copy_status
316 orig = Issue.find(8)
316 orig = Issue.find(8)
317 assert orig.status != IssueStatus.default
317 assert orig.status != IssueStatus.default
318
318
319 issue = Issue.new.copy_from(orig)
319 issue = Issue.new.copy_from(orig)
320 assert issue.save
320 assert issue.save
321 issue.reload
321 issue.reload
322 assert_equal orig.status, issue.status
322 assert_equal orig.status, issue.status
323 end
323 end
324
324
325 def test_should_close_duplicates
325 def test_should_close_duplicates
326 # Create 3 issues
326 # Create 3 issues
327 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
327 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
328 assert issue1.save
328 assert issue1.save
329 issue2 = issue1.clone
329 issue2 = issue1.clone
330 assert issue2.save
330 assert issue2.save
331 issue3 = issue1.clone
331 issue3 = issue1.clone
332 assert issue3.save
332 assert issue3.save
333
333
334 # 2 is a dupe of 1
334 # 2 is a dupe of 1
335 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
335 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
336 # And 3 is a dupe of 2
336 # And 3 is a dupe of 2
337 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
337 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
338 # And 3 is a dupe of 1 (circular duplicates)
338 # And 3 is a dupe of 1 (circular duplicates)
339 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
339 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
340
340
341 assert issue1.reload.duplicates.include?(issue2)
341 assert issue1.reload.duplicates.include?(issue2)
342
342
343 # Closing issue 1
343 # Closing issue 1
344 issue1.init_journal(User.find(:first), "Closing issue1")
344 issue1.init_journal(User.find(:first), "Closing issue1")
345 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
345 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
346 assert issue1.save
346 assert issue1.save
347 # 2 and 3 should be also closed
347 # 2 and 3 should be also closed
348 assert issue2.reload.closed?
348 assert issue2.reload.closed?
349 assert issue3.reload.closed?
349 assert issue3.reload.closed?
350 end
350 end
351
351
352 def test_should_not_close_duplicated_issue
352 def test_should_not_close_duplicated_issue
353 # Create 3 issues
353 # Create 3 issues
354 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
354 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
355 assert issue1.save
355 assert issue1.save
356 issue2 = issue1.clone
356 issue2 = issue1.clone
357 assert issue2.save
357 assert issue2.save
358
358
359 # 2 is a dupe of 1
359 # 2 is a dupe of 1
360 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
360 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
361 # 2 is a dup of 1 but 1 is not a duplicate of 2
361 # 2 is a dup of 1 but 1 is not a duplicate of 2
362 assert !issue2.reload.duplicates.include?(issue1)
362 assert !issue2.reload.duplicates.include?(issue1)
363
363
364 # Closing issue 2
364 # Closing issue 2
365 issue2.init_journal(User.find(:first), "Closing issue2")
365 issue2.init_journal(User.find(:first), "Closing issue2")
366 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
366 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
367 assert issue2.save
367 assert issue2.save
368 # 1 should not be also closed
368 # 1 should not be also closed
369 assert !issue1.reload.closed?
369 assert !issue1.reload.closed?
370 end
370 end
371
371
372 def test_assignable_versions
372 def test_assignable_versions
373 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
373 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
374 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
374 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
375 end
375 end
376
376
377 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
377 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
378 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
378 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
379 assert !issue.save
379 assert !issue.save
380 assert_not_nil issue.errors.on(:fixed_version_id)
380 assert_not_nil issue.errors.on(:fixed_version_id)
381 end
381 end
382
382
383 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
383 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
384 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
384 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
385 assert !issue.save
385 assert !issue.save
386 assert_not_nil issue.errors.on(:fixed_version_id)
386 assert_not_nil issue.errors.on(:fixed_version_id)
387 end
387 end
388
388
389 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
389 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
390 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
390 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
391 assert issue.save
391 assert issue.save
392 end
392 end
393
393
394 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
394 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
395 issue = Issue.find(11)
395 issue = Issue.find(11)
396 assert_equal 'closed', issue.fixed_version.status
396 assert_equal 'closed', issue.fixed_version.status
397 issue.subject = 'Subject changed'
397 issue.subject = 'Subject changed'
398 assert issue.save
398 assert issue.save
399 end
399 end
400
400
401 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
401 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
402 issue = Issue.find(11)
402 issue = Issue.find(11)
403 issue.status_id = 1
403 issue.status_id = 1
404 assert !issue.save
404 assert !issue.save
405 assert_not_nil issue.errors.on_base
405 assert_not_nil issue.errors.on_base
406 end
406 end
407
407
408 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
408 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
409 issue = Issue.find(11)
409 issue = Issue.find(11)
410 issue.status_id = 1
410 issue.status_id = 1
411 issue.fixed_version_id = 3
411 issue.fixed_version_id = 3
412 assert issue.save
412 assert issue.save
413 end
413 end
414
414
415 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
415 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
416 issue = Issue.find(12)
416 issue = Issue.find(12)
417 assert_equal 'locked', issue.fixed_version.status
417 assert_equal 'locked', issue.fixed_version.status
418 issue.status_id = 1
418 issue.status_id = 1
419 assert issue.save
419 assert issue.save
420 end
420 end
421
421
422 def test_move_to_another_project_with_same_category
422 def test_move_to_another_project_with_same_category
423 issue = Issue.find(1)
423 issue = Issue.find(1)
424 assert issue.move_to_project(Project.find(2))
424 assert issue.move_to_project(Project.find(2))
425 issue.reload
425 issue.reload
426 assert_equal 2, issue.project_id
426 assert_equal 2, issue.project_id
427 # Category changes
427 # Category changes
428 assert_equal 4, issue.category_id
428 assert_equal 4, issue.category_id
429 # Make sure time entries were move to the target project
429 # Make sure time entries were move to the target project
430 assert_equal 2, issue.time_entries.first.project_id
430 assert_equal 2, issue.time_entries.first.project_id
431 end
431 end
432
432
433 def test_move_to_another_project_without_same_category
433 def test_move_to_another_project_without_same_category
434 issue = Issue.find(2)
434 issue = Issue.find(2)
435 assert issue.move_to_project(Project.find(2))
435 assert issue.move_to_project(Project.find(2))
436 issue.reload
436 issue.reload
437 assert_equal 2, issue.project_id
437 assert_equal 2, issue.project_id
438 # Category cleared
438 # Category cleared
439 assert_nil issue.category_id
439 assert_nil issue.category_id
440 end
440 end
441
441
442 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
442 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
443 issue = Issue.find(1)
443 issue = Issue.find(1)
444 issue.update_attribute(:fixed_version_id, 1)
444 issue.update_attribute(:fixed_version_id, 1)
445 assert issue.move_to_project(Project.find(2))
445 assert issue.move_to_project(Project.find(2))
446 issue.reload
446 issue.reload
447 assert_equal 2, issue.project_id
447 assert_equal 2, issue.project_id
448 # Cleared fixed_version
448 # Cleared fixed_version
449 assert_equal nil, issue.fixed_version
449 assert_equal nil, issue.fixed_version
450 end
450 end
451
451
452 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
452 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
453 issue = Issue.find(1)
453 issue = Issue.find(1)
454 issue.update_attribute(:fixed_version_id, 4)
454 issue.update_attribute(:fixed_version_id, 4)
455 assert issue.move_to_project(Project.find(5))
455 assert issue.move_to_project(Project.find(5))
456 issue.reload
456 issue.reload
457 assert_equal 5, issue.project_id
457 assert_equal 5, issue.project_id
458 # Keep fixed_version
458 # Keep fixed_version
459 assert_equal 4, issue.fixed_version_id
459 assert_equal 4, issue.fixed_version_id
460 end
460 end
461
461
462 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
462 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
463 issue = Issue.find(1)
463 issue = Issue.find(1)
464 issue.update_attribute(:fixed_version_id, 1)
464 issue.update_attribute(:fixed_version_id, 1)
465 assert issue.move_to_project(Project.find(5))
465 assert issue.move_to_project(Project.find(5))
466 issue.reload
466 issue.reload
467 assert_equal 5, issue.project_id
467 assert_equal 5, issue.project_id
468 # Cleared fixed_version
468 # Cleared fixed_version
469 assert_equal nil, issue.fixed_version
469 assert_equal nil, issue.fixed_version
470 end
470 end
471
471
472 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
472 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
473 issue = Issue.find(1)
473 issue = Issue.find(1)
474 issue.update_attribute(:fixed_version_id, 7)
474 issue.update_attribute(:fixed_version_id, 7)
475 assert issue.move_to_project(Project.find(2))
475 assert issue.move_to_project(Project.find(2))
476 issue.reload
476 issue.reload
477 assert_equal 2, issue.project_id
477 assert_equal 2, issue.project_id
478 # Keep fixed_version
478 # Keep fixed_version
479 assert_equal 7, issue.fixed_version_id
479 assert_equal 7, issue.fixed_version_id
480 end
480 end
481
481
482 def test_move_to_another_project_with_disabled_tracker
482 def test_move_to_another_project_with_disabled_tracker
483 issue = Issue.find(1)
483 issue = Issue.find(1)
484 target = Project.find(2)
484 target = Project.find(2)
485 target.tracker_ids = [3]
485 target.tracker_ids = [3]
486 target.save
486 target.save
487 assert_equal false, issue.move_to_project(target)
487 assert_equal false, issue.move_to_project(target)
488 issue.reload
488 issue.reload
489 assert_equal 1, issue.project_id
489 assert_equal 1, issue.project_id
490 end
490 end
491
491
492 def test_copy_to_the_same_project
492 def test_copy_to_the_same_project
493 issue = Issue.find(1)
493 issue = Issue.find(1)
494 copy = nil
494 copy = nil
495 assert_difference 'Issue.count' do
495 assert_difference 'Issue.count' do
496 copy = issue.move_to_project(issue.project, nil, :copy => true)
496 copy = issue.move_to_project(issue.project, nil, :copy => true)
497 end
497 end
498 assert_kind_of Issue, copy
498 assert_kind_of Issue, copy
499 assert_equal issue.project, copy.project
499 assert_equal issue.project, copy.project
500 assert_equal "125", copy.custom_value_for(2).value
500 assert_equal "125", copy.custom_value_for(2).value
501 end
501 end
502
502
503 def test_copy_to_another_project_and_tracker
503 def test_copy_to_another_project_and_tracker
504 issue = Issue.find(1)
504 issue = Issue.find(1)
505 copy = nil
505 copy = nil
506 assert_difference 'Issue.count' do
506 assert_difference 'Issue.count' do
507 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
507 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
508 end
508 end
509 copy.reload
509 copy.reload
510 assert_kind_of Issue, copy
510 assert_kind_of Issue, copy
511 assert_equal Project.find(3), copy.project
511 assert_equal Project.find(3), copy.project
512 assert_equal Tracker.find(2), copy.tracker
512 assert_equal Tracker.find(2), copy.tracker
513 # Custom field #2 is not associated with target tracker
513 # Custom field #2 is not associated with target tracker
514 assert_nil copy.custom_value_for(2)
514 assert_nil copy.custom_value_for(2)
515 end
515 end
516
516
517 context "#move_to_project" do
517 context "#move_to_project" do
518 context "as a copy" do
518 context "as a copy" do
519 setup do
519 setup do
520 @issue = Issue.find(1)
520 @issue = Issue.find(1)
521 @copy = nil
521 @copy = nil
522 end
522 end
523
523
524 should "not create a journal" do
524 should "not create a journal" do
525 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
525 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
526 assert_equal 0, @copy.reload.journals.size
526 assert_equal 0, @copy.reload.journals.size
527 end
527 end
528
528
529 should "allow assigned_to changes" do
529 should "allow assigned_to changes" do
530 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
530 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
531 assert_equal 3, @copy.assigned_to_id
531 assert_equal 3, @copy.assigned_to_id
532 end
532 end
533
533
534 should "allow status changes" do
534 should "allow status changes" do
535 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
535 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
536 assert_equal 2, @copy.status_id
536 assert_equal 2, @copy.status_id
537 end
537 end
538
538
539 should "allow start date changes" do
539 should "allow start date changes" do
540 date = Date.today
540 date = Date.today
541 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
541 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
542 assert_equal date, @copy.start_date
542 assert_equal date, @copy.start_date
543 end
543 end
544
544
545 should "allow due date changes" do
545 should "allow due date changes" do
546 date = Date.today
546 date = Date.today
547 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
547 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
548
548
549 assert_equal date, @copy.due_date
549 assert_equal date, @copy.due_date
550 end
550 end
551
551
552 should "set current user as author" do
552 should "set current user as author" do
553 User.current = User.find(9)
553 User.current = User.find(9)
554 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {}})
554 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {}})
555
555
556 assert_equal User.current, @copy.author
556 assert_equal User.current, @copy.author
557 end
557 end
558
558
559 should "keep journal notes" do
559 should "keep journal notes" do
560 date = Date.today
560 date = Date.today
561 notes = "Notes added when copying"
561 notes = "Notes added when copying"
562 User.current = User.find(9)
562 User.current = User.find(9)
563 @issue.init_journal(User.current, notes)
563 @issue.init_journal(User.current, notes)
564 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
564 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
565
565
566 assert_equal 1, @copy.journals.size
566 assert_equal 1, @copy.journals.size
567 journal = @copy.journals.first
567 journal = @copy.journals.first
568 assert_equal 0, journal.details.size
568 assert_equal 0, journal.details.size
569 assert_equal notes, journal.notes
569 assert_equal notes, journal.notes
570 end
570 end
571 end
571 end
572 end
572 end
573
573
574 def test_recipients_should_not_include_users_that_cannot_view_the_issue
574 def test_recipients_should_not_include_users_that_cannot_view_the_issue
575 issue = Issue.find(12)
575 issue = Issue.find(12)
576 assert issue.recipients.include?(issue.author.mail)
576 assert issue.recipients.include?(issue.author.mail)
577 # move the issue to a private project
577 # move the issue to a private project
578 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
578 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
579 # author is not a member of project anymore
579 # author is not a member of project anymore
580 assert !copy.recipients.include?(copy.author.mail)
580 assert !copy.recipients.include?(copy.author.mail)
581 end
581 end
582
582
583 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
583 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
584 user = User.find(3)
584 user = User.find(3)
585 issue = Issue.find(9)
585 issue = Issue.find(9)
586 Watcher.create!(:user => user, :watchable => issue)
586 Watcher.create!(:user => user, :watchable => issue)
587 assert issue.watched_by?(user)
587 assert issue.watched_by?(user)
588 assert !issue.watcher_recipients.include?(user.mail)
588 assert !issue.watcher_recipients.include?(user.mail)
589 end
589 end
590
590
591 def test_issue_destroy
591 def test_issue_destroy
592 Issue.find(1).destroy
592 Issue.find(1).destroy
593 assert_nil Issue.find_by_id(1)
593 assert_nil Issue.find_by_id(1)
594 assert_nil TimeEntry.find_by_issue_id(1)
594 assert_nil TimeEntry.find_by_issue_id(1)
595 end
595 end
596
596
597 def test_blocked
597 def test_blocked
598 blocked_issue = Issue.find(9)
598 blocked_issue = Issue.find(9)
599 blocking_issue = Issue.find(10)
599 blocking_issue = Issue.find(10)
600
600
601 assert blocked_issue.blocked?
601 assert blocked_issue.blocked?
602 assert !blocking_issue.blocked?
602 assert !blocking_issue.blocked?
603 end
603 end
604
604
605 def test_blocked_issues_dont_allow_closed_statuses
605 def test_blocked_issues_dont_allow_closed_statuses
606 blocked_issue = Issue.find(9)
606 blocked_issue = Issue.find(9)
607
607
608 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
608 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
609 assert !allowed_statuses.empty?
609 assert !allowed_statuses.empty?
610 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
610 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
611 assert closed_statuses.empty?
611 assert closed_statuses.empty?
612 end
612 end
613
613
614 def test_unblocked_issues_allow_closed_statuses
614 def test_unblocked_issues_allow_closed_statuses
615 blocking_issue = Issue.find(10)
615 blocking_issue = Issue.find(10)
616
616
617 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
617 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
618 assert !allowed_statuses.empty?
618 assert !allowed_statuses.empty?
619 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
619 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
620 assert !closed_statuses.empty?
620 assert !closed_statuses.empty?
621 end
621 end
622
622
623 def test_rescheduling_an_issue_should_reschedule_following_issue
623 def test_rescheduling_an_issue_should_reschedule_following_issue
624 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)
624 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)
625 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)
625 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)
626 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
626 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
627 assert_equal issue1.due_date + 1, issue2.reload.start_date
627 assert_equal issue1.due_date + 1, issue2.reload.start_date
628
628
629 issue1.due_date = Date.today + 5
629 issue1.due_date = Date.today + 5
630 issue1.save!
630 issue1.save!
631 assert_equal issue1.due_date + 1, issue2.reload.start_date
631 assert_equal issue1.due_date + 1, issue2.reload.start_date
632 end
632 end
633
633
634 def test_overdue
634 def test_overdue
635 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
635 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
636 assert !Issue.new(:due_date => Date.today).overdue?
636 assert !Issue.new(:due_date => Date.today).overdue?
637 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
637 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
638 assert !Issue.new(:due_date => nil).overdue?
638 assert !Issue.new(:due_date => nil).overdue?
639 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
639 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
640 end
640 end
641
641
642 context "#behind_schedule?" do
642 context "#behind_schedule?" do
643 should "be false if the issue has no start_date" do
643 should "be false if the issue has no start_date" do
644 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
644 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
645 end
645 end
646
646
647 should "be false if the issue has no end_date" do
647 should "be false if the issue has no end_date" do
648 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
648 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
649 end
649 end
650
650
651 should "be false if the issue has more done than it's calendar time" do
651 should "be false if the issue has more done than it's calendar time" do
652 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
652 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
653 end
653 end
654
654
655 should "be true if the issue hasn't been started at all" do
655 should "be true if the issue hasn't been started at all" do
656 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
656 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
657 end
657 end
658
658
659 should "be true if the issue has used more calendar time than it's done ratio" do
659 should "be true if the issue has used more calendar time than it's done ratio" do
660 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
660 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
661 end
661 end
662 end
662 end
663
663
664 context "#assignable_users" do
664 context "#assignable_users" do
665 should "be Users" do
665 should "be Users" do
666 assert_kind_of User, Issue.find(1).assignable_users.first
666 assert_kind_of User, Issue.find(1).assignable_users.first
667 end
667 end
668
668
669 should "include the issue author" do
669 should "include the issue author" do
670 project = Project.find(1)
670 project = Project.find(1)
671 non_project_member = User.generate!
671 non_project_member = User.generate!
672 issue = Issue.generate_for_project!(project, :author => non_project_member)
672 issue = Issue.generate_for_project!(project, :author => non_project_member)
673
673
674 assert issue.assignable_users.include?(non_project_member)
674 assert issue.assignable_users.include?(non_project_member)
675 end
675 end
676
676
677 should "not show the issue author twice" do
677 should "not show the issue author twice" do
678 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
678 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
679 assert_equal 2, assignable_user_ids.length
679 assert_equal 2, assignable_user_ids.length
680
680
681 assignable_user_ids.each do |user_id|
681 assignable_user_ids.each do |user_id|
682 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
682 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
683 end
683 end
684 end
684 end
685 end
685 end
686
686
687 def test_create_should_send_email_notification
687 def test_create_should_send_email_notification
688 ActionMailer::Base.deliveries.clear
688 ActionMailer::Base.deliveries.clear
689 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
689 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
690
690
691 assert issue.save
691 assert issue.save
692 assert_equal 1, ActionMailer::Base.deliveries.size
692 assert_equal 1, ActionMailer::Base.deliveries.size
693 end
693 end
694
694
695 def test_stale_issue_should_not_send_email_notification
695 def test_stale_issue_should_not_send_email_notification
696 ActionMailer::Base.deliveries.clear
696 ActionMailer::Base.deliveries.clear
697 issue = Issue.find(1)
697 issue = Issue.find(1)
698 stale = Issue.find(1)
698 stale = Issue.find(1)
699
699
700 issue.init_journal(User.find(1))
700 issue.init_journal(User.find(1))
701 issue.subject = 'Subjet update'
701 issue.subject = 'Subjet update'
702 assert issue.save
702 assert issue.save
703 assert_equal 1, ActionMailer::Base.deliveries.size
703 assert_equal 1, ActionMailer::Base.deliveries.size
704 ActionMailer::Base.deliveries.clear
704 ActionMailer::Base.deliveries.clear
705
705
706 stale.init_journal(User.find(1))
706 stale.init_journal(User.find(1))
707 stale.subject = 'Another subjet update'
707 stale.subject = 'Another subjet update'
708 assert_raise ActiveRecord::StaleObjectError do
708 assert_raise ActiveRecord::StaleObjectError do
709 stale.save
709 stale.save
710 end
710 end
711 assert ActionMailer::Base.deliveries.empty?
711 assert ActionMailer::Base.deliveries.empty?
712 end
712 end
713
713
714 def test_journalized_description
714 def test_journalized_description
715 IssueCustomField.delete_all
715 IssueCustomField.delete_all
716
716
717 i = Issue.first
717 i = Issue.first
718 old_description = i.description
718 old_description = i.description
719 new_description = "This is the new description"
719 new_description = "This is the new description"
720
720
721 i.init_journal(User.find(2))
721 i.init_journal(User.find(2))
722 i.description = new_description
722 i.description = new_description
723 assert_difference 'Journal.count', 1 do
723 assert_difference 'Journal.count', 1 do
724 assert_difference 'JournalDetail.count', 1 do
724 assert_difference 'JournalDetail.count', 1 do
725 i.save!
725 i.save!
726 end
726 end
727 end
727 end
728
728
729 detail = JournalDetail.first(:order => 'id DESC')
729 detail = JournalDetail.first(:order => 'id DESC')
730 assert_equal i, detail.journal.journalized
730 assert_equal i, detail.journal.journalized
731 assert_equal 'attr', detail.property
731 assert_equal 'attr', detail.property
732 assert_equal 'description', detail.prop_key
732 assert_equal 'description', detail.prop_key
733 assert_equal old_description, detail.old_value
733 assert_equal old_description, detail.old_value
734 assert_equal new_description, detail.value
734 assert_equal new_description, detail.value
735 end
735 end
736
736
737 def test_blank_descriptions_should_not_be_journalized
738 IssueCustomField.delete_all
739 Issue.update_all("description = NULL", "id=1")
740
741 i = Issue.find(1)
742 i.init_journal(User.find(2))
743 i.subject = "blank description"
744 i.description = "\r\n"
745
746 assert_difference 'Journal.count', 1 do
747 assert_difference 'JournalDetail.count', 1 do
748 i.save!
749 end
750 end
751 end
752
753 def test_description_eol_should_be_normalized
754 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
755 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
756 end
757
737 def test_saving_twice_should_not_duplicate_journal_details
758 def test_saving_twice_should_not_duplicate_journal_details
738 i = Issue.find(:first)
759 i = Issue.find(:first)
739 i.init_journal(User.find(2), 'Some notes')
760 i.init_journal(User.find(2), 'Some notes')
740 # initial changes
761 # initial changes
741 i.subject = 'New subject'
762 i.subject = 'New subject'
742 i.done_ratio = i.done_ratio + 10
763 i.done_ratio = i.done_ratio + 10
743 assert_difference 'Journal.count' do
764 assert_difference 'Journal.count' do
744 assert i.save
765 assert i.save
745 end
766 end
746 # 1 more change
767 # 1 more change
747 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
768 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
748 assert_no_difference 'Journal.count' do
769 assert_no_difference 'Journal.count' do
749 assert_difference 'JournalDetail.count', 1 do
770 assert_difference 'JournalDetail.count', 1 do
750 i.save
771 i.save
751 end
772 end
752 end
773 end
753 # no more change
774 # no more change
754 assert_no_difference 'Journal.count' do
775 assert_no_difference 'Journal.count' do
755 assert_no_difference 'JournalDetail.count' do
776 assert_no_difference 'JournalDetail.count' do
756 i.save
777 i.save
757 end
778 end
758 end
779 end
759 end
780 end
760
781
761 def test_all_dependent_issues
782 def test_all_dependent_issues
762 IssueRelation.delete_all
783 IssueRelation.delete_all
763 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
784 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
764 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
785 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
765 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
786 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
766
787
767 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
788 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
768 end
789 end
769
790
770 def test_all_dependent_issues_with_persistent_circular_dependency
791 def test_all_dependent_issues_with_persistent_circular_dependency
771 IssueRelation.delete_all
792 IssueRelation.delete_all
772 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
793 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
773 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
794 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
774 # Validation skipping
795 # Validation skipping
775 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
796 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
776
797
777 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
798 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
778 end
799 end
779
800
780 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
801 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
781 IssueRelation.delete_all
802 IssueRelation.delete_all
782 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES)
803 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES)
783 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_RELATES)
804 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_RELATES)
784 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_RELATES)
805 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_RELATES)
785 # Validation skipping
806 # Validation skipping
786 assert IssueRelation.new(:issue_from => Issue.find(8), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES).save(false)
807 assert IssueRelation.new(:issue_from => Issue.find(8), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_RELATES).save(false)
787 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_RELATES).save(false)
808 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_RELATES).save(false)
788
809
789 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
810 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
790 end
811 end
791
812
792 context "#done_ratio" do
813 context "#done_ratio" do
793 setup do
814 setup do
794 @issue = Issue.find(1)
815 @issue = Issue.find(1)
795 @issue_status = IssueStatus.find(1)
816 @issue_status = IssueStatus.find(1)
796 @issue_status.update_attribute(:default_done_ratio, 50)
817 @issue_status.update_attribute(:default_done_ratio, 50)
797 @issue2 = Issue.find(2)
818 @issue2 = Issue.find(2)
798 @issue_status2 = IssueStatus.find(2)
819 @issue_status2 = IssueStatus.find(2)
799 @issue_status2.update_attribute(:default_done_ratio, 0)
820 @issue_status2.update_attribute(:default_done_ratio, 0)
800 end
821 end
801
822
802 context "with Setting.issue_done_ratio using the issue_field" do
823 context "with Setting.issue_done_ratio using the issue_field" do
803 setup do
824 setup do
804 Setting.issue_done_ratio = 'issue_field'
825 Setting.issue_done_ratio = 'issue_field'
805 end
826 end
806
827
807 should "read the issue's field" do
828 should "read the issue's field" do
808 assert_equal 0, @issue.done_ratio
829 assert_equal 0, @issue.done_ratio
809 assert_equal 30, @issue2.done_ratio
830 assert_equal 30, @issue2.done_ratio
810 end
831 end
811 end
832 end
812
833
813 context "with Setting.issue_done_ratio using the issue_status" do
834 context "with Setting.issue_done_ratio using the issue_status" do
814 setup do
835 setup do
815 Setting.issue_done_ratio = 'issue_status'
836 Setting.issue_done_ratio = 'issue_status'
816 end
837 end
817
838
818 should "read the Issue Status's default done ratio" do
839 should "read the Issue Status's default done ratio" do
819 assert_equal 50, @issue.done_ratio
840 assert_equal 50, @issue.done_ratio
820 assert_equal 0, @issue2.done_ratio
841 assert_equal 0, @issue2.done_ratio
821 end
842 end
822 end
843 end
823 end
844 end
824
845
825 context "#update_done_ratio_from_issue_status" do
846 context "#update_done_ratio_from_issue_status" do
826 setup do
847 setup do
827 @issue = Issue.find(1)
848 @issue = Issue.find(1)
828 @issue_status = IssueStatus.find(1)
849 @issue_status = IssueStatus.find(1)
829 @issue_status.update_attribute(:default_done_ratio, 50)
850 @issue_status.update_attribute(:default_done_ratio, 50)
830 @issue2 = Issue.find(2)
851 @issue2 = Issue.find(2)
831 @issue_status2 = IssueStatus.find(2)
852 @issue_status2 = IssueStatus.find(2)
832 @issue_status2.update_attribute(:default_done_ratio, 0)
853 @issue_status2.update_attribute(:default_done_ratio, 0)
833 end
854 end
834
855
835 context "with Setting.issue_done_ratio using the issue_field" do
856 context "with Setting.issue_done_ratio using the issue_field" do
836 setup do
857 setup do
837 Setting.issue_done_ratio = 'issue_field'
858 Setting.issue_done_ratio = 'issue_field'
838 end
859 end
839
860
840 should "not change the issue" do
861 should "not change the issue" do
841 @issue.update_done_ratio_from_issue_status
862 @issue.update_done_ratio_from_issue_status
842 @issue2.update_done_ratio_from_issue_status
863 @issue2.update_done_ratio_from_issue_status
843
864
844 assert_equal 0, @issue.read_attribute(:done_ratio)
865 assert_equal 0, @issue.read_attribute(:done_ratio)
845 assert_equal 30, @issue2.read_attribute(:done_ratio)
866 assert_equal 30, @issue2.read_attribute(:done_ratio)
846 end
867 end
847 end
868 end
848
869
849 context "with Setting.issue_done_ratio using the issue_status" do
870 context "with Setting.issue_done_ratio using the issue_status" do
850 setup do
871 setup do
851 Setting.issue_done_ratio = 'issue_status'
872 Setting.issue_done_ratio = 'issue_status'
852 end
873 end
853
874
854 should "change the issue's done ratio" do
875 should "change the issue's done ratio" do
855 @issue.update_done_ratio_from_issue_status
876 @issue.update_done_ratio_from_issue_status
856 @issue2.update_done_ratio_from_issue_status
877 @issue2.update_done_ratio_from_issue_status
857
878
858 assert_equal 50, @issue.read_attribute(:done_ratio)
879 assert_equal 50, @issue.read_attribute(:done_ratio)
859 assert_equal 0, @issue2.read_attribute(:done_ratio)
880 assert_equal 0, @issue2.read_attribute(:done_ratio)
860 end
881 end
861 end
882 end
862 end
883 end
863
884
864 test "#by_tracker" do
885 test "#by_tracker" do
865 User.current = User.anonymous
886 User.current = User.anonymous
866 groups = Issue.by_tracker(Project.find(1))
887 groups = Issue.by_tracker(Project.find(1))
867 assert_equal 3, groups.size
888 assert_equal 3, groups.size
868 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
889 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
869 end
890 end
870
891
871 test "#by_version" do
892 test "#by_version" do
872 User.current = User.anonymous
893 User.current = User.anonymous
873 groups = Issue.by_version(Project.find(1))
894 groups = Issue.by_version(Project.find(1))
874 assert_equal 3, groups.size
895 assert_equal 3, groups.size
875 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
896 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
876 end
897 end
877
898
878 test "#by_priority" do
899 test "#by_priority" do
879 User.current = User.anonymous
900 User.current = User.anonymous
880 groups = Issue.by_priority(Project.find(1))
901 groups = Issue.by_priority(Project.find(1))
881 assert_equal 4, groups.size
902 assert_equal 4, groups.size
882 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
903 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
883 end
904 end
884
905
885 test "#by_category" do
906 test "#by_category" do
886 User.current = User.anonymous
907 User.current = User.anonymous
887 groups = Issue.by_category(Project.find(1))
908 groups = Issue.by_category(Project.find(1))
888 assert_equal 2, groups.size
909 assert_equal 2, groups.size
889 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
910 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
890 end
911 end
891
912
892 test "#by_assigned_to" do
913 test "#by_assigned_to" do
893 User.current = User.anonymous
914 User.current = User.anonymous
894 groups = Issue.by_assigned_to(Project.find(1))
915 groups = Issue.by_assigned_to(Project.find(1))
895 assert_equal 2, groups.size
916 assert_equal 2, groups.size
896 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
917 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
897 end
918 end
898
919
899 test "#by_author" do
920 test "#by_author" do
900 User.current = User.anonymous
921 User.current = User.anonymous
901 groups = Issue.by_author(Project.find(1))
922 groups = Issue.by_author(Project.find(1))
902 assert_equal 4, groups.size
923 assert_equal 4, groups.size
903 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
924 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
904 end
925 end
905
926
906 test "#by_subproject" do
927 test "#by_subproject" do
907 User.current = User.anonymous
928 User.current = User.anonymous
908 groups = Issue.by_subproject(Project.find(1))
929 groups = Issue.by_subproject(Project.find(1))
909 # Private descendant not visible
930 # Private descendant not visible
910 assert_equal 1, groups.size
931 assert_equal 1, groups.size
911 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
932 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
912 end
933 end
913
934
914 context ".allowed_target_projects_on_move" do
935 context ".allowed_target_projects_on_move" do
915 should "return all active projects for admin users" do
936 should "return all active projects for admin users" do
916 User.current = User.find(1)
937 User.current = User.find(1)
917 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
938 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
918 end
939 end
919
940
920 should "return allowed projects for non admin users" do
941 should "return allowed projects for non admin users" do
921 User.current = User.find(2)
942 User.current = User.find(2)
922 Role.non_member.remove_permission! :move_issues
943 Role.non_member.remove_permission! :move_issues
923 assert_equal 3, Issue.allowed_target_projects_on_move.size
944 assert_equal 3, Issue.allowed_target_projects_on_move.size
924
945
925 Role.non_member.add_permission! :move_issues
946 Role.non_member.add_permission! :move_issues
926 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
947 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
927 end
948 end
928 end
949 end
929
950
930 def test_recently_updated_with_limit_scopes
951 def test_recently_updated_with_limit_scopes
931 #should return the last updated issue
952 #should return the last updated issue
932 assert_equal 1, Issue.recently_updated.with_limit(1).length
953 assert_equal 1, Issue.recently_updated.with_limit(1).length
933 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
954 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
934 end
955 end
935
956
936 def test_on_active_projects_scope
957 def test_on_active_projects_scope
937 assert Project.find(2).archive
958 assert Project.find(2).archive
938
959
939 before = Issue.on_active_project.length
960 before = Issue.on_active_project.length
940 # test inclusion to results
961 # test inclusion to results
941 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
962 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
942 assert_equal before + 1, Issue.on_active_project.length
963 assert_equal before + 1, Issue.on_active_project.length
943
964
944 # Move to an archived project
965 # Move to an archived project
945 issue.project = Project.find(2)
966 issue.project = Project.find(2)
946 assert issue.save
967 assert issue.save
947 assert_equal before, Issue.on_active_project.length
968 assert_equal before, Issue.on_active_project.length
948 end
969 end
949
970
950 context "Issue#recipients" do
971 context "Issue#recipients" do
951 setup do
972 setup do
952 @project = Project.find(1)
973 @project = Project.find(1)
953 @author = User.generate_with_protected!
974 @author = User.generate_with_protected!
954 @assignee = User.generate_with_protected!
975 @assignee = User.generate_with_protected!
955 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
976 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
956 end
977 end
957
978
958 should "include project recipients" do
979 should "include project recipients" do
959 assert @project.recipients.present?
980 assert @project.recipients.present?
960 @project.recipients.each do |project_recipient|
981 @project.recipients.each do |project_recipient|
961 assert @issue.recipients.include?(project_recipient)
982 assert @issue.recipients.include?(project_recipient)
962 end
983 end
963 end
984 end
964
985
965 should "include the author if the author is active" do
986 should "include the author if the author is active" do
966 assert @issue.author, "No author set for Issue"
987 assert @issue.author, "No author set for Issue"
967 assert @issue.recipients.include?(@issue.author.mail)
988 assert @issue.recipients.include?(@issue.author.mail)
968 end
989 end
969
990
970 should "include the assigned to user if the assigned to user is active" do
991 should "include the assigned to user if the assigned to user is active" do
971 assert @issue.assigned_to, "No assigned_to set for Issue"
992 assert @issue.assigned_to, "No assigned_to set for Issue"
972 assert @issue.recipients.include?(@issue.assigned_to.mail)
993 assert @issue.recipients.include?(@issue.assigned_to.mail)
973 end
994 end
974
995
975 should "not include users who opt out of all email" do
996 should "not include users who opt out of all email" do
976 @author.update_attribute(:mail_notification, :none)
997 @author.update_attribute(:mail_notification, :none)
977
998
978 assert !@issue.recipients.include?(@issue.author.mail)
999 assert !@issue.recipients.include?(@issue.author.mail)
979 end
1000 end
980
1001
981 should "not include the issue author if they are only notified of assigned issues" do
1002 should "not include the issue author if they are only notified of assigned issues" do
982 @author.update_attribute(:mail_notification, :only_assigned)
1003 @author.update_attribute(:mail_notification, :only_assigned)
983
1004
984 assert !@issue.recipients.include?(@issue.author.mail)
1005 assert !@issue.recipients.include?(@issue.author.mail)
985 end
1006 end
986
1007
987 should "not include the assigned user if they are only notified of owned issues" do
1008 should "not include the assigned user if they are only notified of owned issues" do
988 @assignee.update_attribute(:mail_notification, :only_owner)
1009 @assignee.update_attribute(:mail_notification, :only_owner)
989
1010
990 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1011 assert !@issue.recipients.include?(@issue.assigned_to.mail)
991 end
1012 end
992
1013
993 end
1014 end
994 end
1015 end
General Comments 0
You need to be logged in to leave comments. Login now