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