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