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