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