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