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