##// END OF EJS Templates
Copy issue status on project copy (#3877)....
Jean-Philippe Lang -
r2961:3fc655904f90
parent child
Show More
@@ -1,347 +1,348
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 29 has_many :time_entries, :dependent => :delete_all
30 30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31 31
32 32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34 34
35 35 acts_as_attachable :after_remove => :attachment_removed
36 36 acts_as_customizable
37 37 acts_as_watchable
38 38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 39 :include => [:project, :journals],
40 40 # sort by id so that limited eager loading doesn't break with postgresql
41 41 :order_column => "#{table_name}.id"
42 42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 45
46 46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 47 :author_key => :author_id
48 48
49 49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 50 validates_length_of :subject, :maximum => 255
51 51 validates_inclusion_of :done_ratio, :in => 0..100
52 52 validates_numericality_of :estimated_hours, :allow_nil => true
53 53
54 54 named_scope :visible, lambda {|*args| { :include => :project,
55 55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56 56
57 57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
58 58
59 59 after_save :create_journal
60 60
61 61 # Returns true if usr or current user is allowed to view the issue
62 62 def visible?(usr=nil)
63 63 (usr || User.current).allowed_to?(:view_issues, self.project)
64 64 end
65 65
66 66 def after_initialize
67 67 if new_record?
68 68 # set default values for new records only
69 69 self.status ||= IssueStatus.default
70 70 self.priority ||= IssuePriority.default
71 71 end
72 72 end
73 73
74 74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
75 75 def available_custom_fields
76 76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
77 77 end
78 78
79 79 def copy_from(arg)
80 80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
81 81 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
82 82 self.custom_values = issue.custom_values.collect {|v| v.clone}
83 self.status = issue.status
83 84 self
84 85 end
85 86
86 87 # Moves/copies an issue to a new project and tracker
87 88 # Returns the moved/copied issue on success, false on failure
88 89 def move_to(new_project, new_tracker = nil, options = {})
89 90 options ||= {}
90 91 issue = options[:copy] ? self.clone : self
91 92 transaction do
92 93 if new_project && issue.project_id != new_project.id
93 94 # delete issue relations
94 95 unless Setting.cross_project_issue_relations?
95 96 issue.relations_from.clear
96 97 issue.relations_to.clear
97 98 end
98 99 # issue is moved to another project
99 100 # reassign to the category with same name if any
100 101 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
101 102 issue.category = new_category
102 103 issue.fixed_version = nil
103 104 issue.project = new_project
104 105 end
105 106 if new_tracker
106 107 issue.tracker = new_tracker
107 108 end
108 109 if options[:copy]
109 110 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
110 111 issue.status = self.status
111 112 end
112 113 if issue.save
113 114 unless options[:copy]
114 115 # Manually update project_id on related time entries
115 116 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
116 117 end
117 118 else
118 119 Issue.connection.rollback_db_transaction
119 120 return false
120 121 end
121 122 end
122 123 return issue
123 124 end
124 125
125 126 def priority_id=(pid)
126 127 self.priority = nil
127 128 write_attribute(:priority_id, pid)
128 129 end
129 130
130 131 def estimated_hours=(h)
131 132 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
132 133 end
133 134
134 135 def validate
135 136 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
136 137 errors.add :due_date, :not_a_date
137 138 end
138 139
139 140 if self.due_date and self.start_date and self.due_date < self.start_date
140 141 errors.add :due_date, :greater_than_start_date
141 142 end
142 143
143 144 if start_date && soonest_start && start_date < soonest_start
144 145 errors.add :start_date, :invalid
145 146 end
146 147
147 148 if fixed_version
148 149 if !assignable_versions.include?(fixed_version)
149 150 errors.add :fixed_version_id, :inclusion
150 151 elsif reopened? && fixed_version.closed?
151 152 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
152 153 end
153 154 end
154 155 end
155 156
156 157 def validate_on_create
157 158 errors.add :tracker_id, :invalid unless project.trackers.include?(tracker)
158 159 end
159 160
160 161 def before_create
161 162 # default assignment based on category
162 163 if assigned_to.nil? && category && category.assigned_to
163 164 self.assigned_to = category.assigned_to
164 165 end
165 166 end
166 167
167 168 def after_save
168 169 # Reload is needed in order to get the right status
169 170 reload
170 171
171 172 # Update start/due dates of following issues
172 173 relations_from.each(&:set_issue_to_dates)
173 174
174 175 # Close duplicates if the issue was closed
175 176 if @issue_before_change && !@issue_before_change.closed? && self.closed?
176 177 duplicates.each do |duplicate|
177 178 # Reload is need in case the duplicate was updated by a previous duplicate
178 179 duplicate.reload
179 180 # Don't re-close it if it's already closed
180 181 next if duplicate.closed?
181 182 # Same user and notes
182 183 duplicate.init_journal(@current_journal.user, @current_journal.notes)
183 184 duplicate.update_attribute :status, self.status
184 185 end
185 186 end
186 187 end
187 188
188 189 def init_journal(user, notes = "")
189 190 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
190 191 @issue_before_change = self.clone
191 192 @issue_before_change.status = self.status
192 193 @custom_values_before_change = {}
193 194 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
194 195 # Make sure updated_on is updated when adding a note.
195 196 updated_on_will_change!
196 197 @current_journal
197 198 end
198 199
199 200 # Return true if the issue is closed, otherwise false
200 201 def closed?
201 202 self.status.is_closed?
202 203 end
203 204
204 205 # Return true if the issue is being reopened
205 206 def reopened?
206 207 if !new_record? && status_id_changed?
207 208 status_was = IssueStatus.find_by_id(status_id_was)
208 209 status_new = IssueStatus.find_by_id(status_id)
209 210 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
210 211 return true
211 212 end
212 213 end
213 214 false
214 215 end
215 216
216 217 # Returns true if the issue is overdue
217 218 def overdue?
218 219 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
219 220 end
220 221
221 222 # Users the issue can be assigned to
222 223 def assignable_users
223 224 project.assignable_users
224 225 end
225 226
226 227 # Versions that the issue can be assigned to
227 228 def assignable_versions
228 229 @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
229 230 end
230 231
231 232 # Returns true if this issue is blocked by another issue that is still open
232 233 def blocked?
233 234 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
234 235 end
235 236
236 237 # Returns an array of status that user is able to apply
237 238 def new_statuses_allowed_to(user)
238 239 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
239 240 statuses << status unless statuses.empty?
240 241 statuses = statuses.uniq.sort
241 242 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
242 243 end
243 244
244 245 # Returns the mail adresses of users that should be notified for the issue
245 246 def recipients
246 247 recipients = project.recipients
247 248 # Author and assignee are always notified unless they have been locked
248 249 recipients << author.mail if author && author.active?
249 250 recipients << assigned_to.mail if assigned_to && assigned_to.active?
250 251 recipients.compact.uniq
251 252 end
252 253
253 254 # Returns the total number of hours spent on this issue.
254 255 #
255 256 # Example:
256 257 # spent_hours => 0
257 258 # spent_hours => 50
258 259 def spent_hours
259 260 @spent_hours ||= time_entries.sum(:hours) || 0
260 261 end
261 262
262 263 def relations
263 264 (relations_from + relations_to).sort
264 265 end
265 266
266 267 def all_dependent_issues
267 268 dependencies = []
268 269 relations_from.each do |relation|
269 270 dependencies << relation.issue_to
270 271 dependencies += relation.issue_to.all_dependent_issues
271 272 end
272 273 dependencies
273 274 end
274 275
275 276 # Returns an array of issues that duplicate this one
276 277 def duplicates
277 278 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
278 279 end
279 280
280 281 # Returns the due date or the target due date if any
281 282 # Used on gantt chart
282 283 def due_before
283 284 due_date || (fixed_version ? fixed_version.effective_date : nil)
284 285 end
285 286
286 287 # Returns the time scheduled for this issue.
287 288 #
288 289 # Example:
289 290 # Start Date: 2/26/09, End Date: 3/04/09
290 291 # duration => 6
291 292 def duration
292 293 (start_date && due_date) ? due_date - start_date : 0
293 294 end
294 295
295 296 def soonest_start
296 297 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
297 298 end
298 299
299 300 def to_s
300 301 "#{tracker} ##{id}: #{subject}"
301 302 end
302 303
303 304 # Returns a string of css classes that apply to the issue
304 305 def css_classes
305 306 s = "issue status-#{status.position} priority-#{priority.position}"
306 307 s << ' closed' if closed?
307 308 s << ' overdue' if overdue?
308 309 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
309 310 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
310 311 s
311 312 end
312 313
313 314 private
314 315
315 316 # Callback on attachment deletion
316 317 def attachment_removed(obj)
317 318 journal = init_journal(User.current)
318 319 journal.details << JournalDetail.new(:property => 'attachment',
319 320 :prop_key => obj.id,
320 321 :old_value => obj.filename)
321 322 journal.save
322 323 end
323 324
324 325 # Saves the changes in a Journal
325 326 # Called after_save
326 327 def create_journal
327 328 if @current_journal
328 329 # attributes changes
329 330 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
330 331 @current_journal.details << JournalDetail.new(:property => 'attr',
331 332 :prop_key => c,
332 333 :old_value => @issue_before_change.send(c),
333 334 :value => send(c)) unless send(c)==@issue_before_change.send(c)
334 335 }
335 336 # custom fields changes
336 337 custom_values.each {|c|
337 338 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
338 339 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
339 340 @current_journal.details << JournalDetail.new(:property => 'cf',
340 341 :prop_key => c.custom_field_id,
341 342 :old_value => @custom_values_before_change[c.custom_field_id],
342 343 :value => c.value)
343 344 }
344 345 @current_journal.save
345 346 end
346 347 end
347 348 end
@@ -1,393 +1,403
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :versions,
24 24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 25 :enumerations,
26 26 :issues,
27 27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 28 :time_entries
29 29
30 30 def test_create
31 31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 32 assert issue.save
33 33 issue.reload
34 34 assert_equal 1.5, issue.estimated_hours
35 35 end
36 36
37 37 def test_create_minimal
38 38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 39 assert issue.save
40 40 assert issue.description.nil?
41 41 end
42 42
43 43 def test_create_with_required_custom_field
44 44 field = IssueCustomField.find_by_name('Database')
45 45 field.update_attribute(:is_required, true)
46 46
47 47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 48 assert issue.available_custom_fields.include?(field)
49 49 # No value for the custom field
50 50 assert !issue.save
51 51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 52 # Blank value
53 53 issue.custom_field_values = { field.id => '' }
54 54 assert !issue.save
55 55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 56 # Invalid value
57 57 issue.custom_field_values = { field.id => 'SQLServer' }
58 58 assert !issue.save
59 59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 60 # Valid value
61 61 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 62 assert issue.save
63 63 issue.reload
64 64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 65 end
66 66
67 67 def test_visible_scope_for_anonymous
68 68 # Anonymous user should see issues of public projects only
69 69 issues = Issue.visible(User.anonymous).all
70 70 assert issues.any?
71 71 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 72 # Anonymous user should not see issues without permission
73 73 Role.anonymous.remove_permission!(:view_issues)
74 74 issues = Issue.visible(User.anonymous).all
75 75 assert issues.empty?
76 76 end
77 77
78 78 def test_visible_scope_for_user
79 79 user = User.find(9)
80 80 assert user.projects.empty?
81 81 # Non member user should see issues of public projects only
82 82 issues = Issue.visible(user).all
83 83 assert issues.any?
84 84 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 85 # Non member user should not see issues without permission
86 86 Role.non_member.remove_permission!(:view_issues)
87 87 user.reload
88 88 issues = Issue.visible(user).all
89 89 assert issues.empty?
90 90 # User should see issues of projects for which he has view_issues permissions only
91 91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 92 user.reload
93 93 issues = Issue.visible(user).all
94 94 assert issues.any?
95 95 assert_nil issues.detect {|issue| issue.project_id != 2}
96 96 end
97 97
98 98 def test_visible_scope_for_admin
99 99 user = User.find(1)
100 100 user.members.each(&:destroy)
101 101 assert user.projects.empty?
102 102 issues = Issue.visible(user).all
103 103 assert issues.any?
104 104 # Admin should see issues on private projects that he does not belong to
105 105 assert issues.detect {|issue| !issue.project.is_public?}
106 106 end
107 107
108 108 def test_errors_full_messages_should_include_custom_fields_errors
109 109 field = IssueCustomField.find_by_name('Database')
110 110
111 111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
112 112 assert issue.available_custom_fields.include?(field)
113 113 # Invalid value
114 114 issue.custom_field_values = { field.id => 'SQLServer' }
115 115
116 116 assert !issue.valid?
117 117 assert_equal 1, issue.errors.full_messages.size
118 118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 119 end
120 120
121 121 def test_update_issue_with_required_custom_field
122 122 field = IssueCustomField.find_by_name('Database')
123 123 field.update_attribute(:is_required, true)
124 124
125 125 issue = Issue.find(1)
126 126 assert_nil issue.custom_value_for(field)
127 127 assert issue.available_custom_fields.include?(field)
128 128 # No change to custom values, issue can be saved
129 129 assert issue.save
130 130 # Blank value
131 131 issue.custom_field_values = { field.id => '' }
132 132 assert !issue.save
133 133 # Valid value
134 134 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 135 assert issue.save
136 136 issue.reload
137 137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 138 end
139 139
140 140 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 141 issue = Issue.find(1)
142 142 field = IssueCustomField.find_by_name('Database')
143 143 assert issue.available_custom_fields.include?(field)
144 144
145 145 issue.custom_field_values = { field.id => 'Invalid' }
146 146 issue.subject = 'Should be not be saved'
147 147 assert !issue.save
148 148
149 149 issue.reload
150 150 assert_equal "Can't print recipes", issue.subject
151 151 end
152 152
153 153 def test_should_not_recreate_custom_values_objects_on_update
154 154 field = IssueCustomField.find_by_name('Database')
155 155
156 156 issue = Issue.find(1)
157 157 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 158 assert issue.save
159 159 custom_value = issue.custom_value_for(field)
160 160 issue.reload
161 161 issue.custom_field_values = { field.id => 'MySQL' }
162 162 assert issue.save
163 163 issue.reload
164 164 assert_equal custom_value.id, issue.custom_value_for(field).id
165 165 end
166 166
167 167 def test_category_based_assignment
168 168 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
169 169 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
170 170 end
171 171
172 172 def test_copy
173 173 issue = Issue.new.copy_from(1)
174 174 assert issue.save
175 175 issue.reload
176 176 orig = Issue.find(1)
177 177 assert_equal orig.subject, issue.subject
178 178 assert_equal orig.tracker, issue.tracker
179 179 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
180 180 end
181
182 def test_copy_should_copy_status
183 orig = Issue.find(8)
184 assert orig.status != IssueStatus.default
185
186 issue = Issue.new.copy_from(orig)
187 assert issue.save
188 issue.reload
189 assert_equal orig.status, issue.status
190 end
181 191
182 192 def test_should_close_duplicates
183 193 # Create 3 issues
184 194 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
185 195 assert issue1.save
186 196 issue2 = issue1.clone
187 197 assert issue2.save
188 198 issue3 = issue1.clone
189 199 assert issue3.save
190 200
191 201 # 2 is a dupe of 1
192 202 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
193 203 # And 3 is a dupe of 2
194 204 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
195 205 # And 3 is a dupe of 1 (circular duplicates)
196 206 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
197 207
198 208 assert issue1.reload.duplicates.include?(issue2)
199 209
200 210 # Closing issue 1
201 211 issue1.init_journal(User.find(:first), "Closing issue1")
202 212 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
203 213 assert issue1.save
204 214 # 2 and 3 should be also closed
205 215 assert issue2.reload.closed?
206 216 assert issue3.reload.closed?
207 217 end
208 218
209 219 def test_should_not_close_duplicated_issue
210 220 # Create 3 issues
211 221 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
212 222 assert issue1.save
213 223 issue2 = issue1.clone
214 224 assert issue2.save
215 225
216 226 # 2 is a dupe of 1
217 227 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
218 228 # 2 is a dup of 1 but 1 is not a duplicate of 2
219 229 assert !issue2.reload.duplicates.include?(issue1)
220 230
221 231 # Closing issue 2
222 232 issue2.init_journal(User.find(:first), "Closing issue2")
223 233 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
224 234 assert issue2.save
225 235 # 1 should not be also closed
226 236 assert !issue1.reload.closed?
227 237 end
228 238
229 239 def test_assignable_versions
230 240 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
231 241 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
232 242 end
233 243
234 244 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
235 245 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
236 246 assert !issue.save
237 247 assert_not_nil issue.errors.on(:fixed_version_id)
238 248 end
239 249
240 250 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
241 251 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
242 252 assert !issue.save
243 253 assert_not_nil issue.errors.on(:fixed_version_id)
244 254 end
245 255
246 256 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
247 257 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
248 258 assert issue.save
249 259 end
250 260
251 261 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
252 262 issue = Issue.find(11)
253 263 assert_equal 'closed', issue.fixed_version.status
254 264 issue.subject = 'Subject changed'
255 265 assert issue.save
256 266 end
257 267
258 268 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
259 269 issue = Issue.find(11)
260 270 issue.status_id = 1
261 271 assert !issue.save
262 272 assert_not_nil issue.errors.on_base
263 273 end
264 274
265 275 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
266 276 issue = Issue.find(11)
267 277 issue.status_id = 1
268 278 issue.fixed_version_id = 3
269 279 assert issue.save
270 280 end
271 281
272 282 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
273 283 issue = Issue.find(12)
274 284 assert_equal 'locked', issue.fixed_version.status
275 285 issue.status_id = 1
276 286 assert issue.save
277 287 end
278 288
279 289 def test_move_to_another_project_with_same_category
280 290 issue = Issue.find(1)
281 291 assert issue.move_to(Project.find(2))
282 292 issue.reload
283 293 assert_equal 2, issue.project_id
284 294 # Category changes
285 295 assert_equal 4, issue.category_id
286 296 # Make sure time entries were move to the target project
287 297 assert_equal 2, issue.time_entries.first.project_id
288 298 end
289 299
290 300 def test_move_to_another_project_without_same_category
291 301 issue = Issue.find(2)
292 302 assert issue.move_to(Project.find(2))
293 303 issue.reload
294 304 assert_equal 2, issue.project_id
295 305 # Category cleared
296 306 assert_nil issue.category_id
297 307 end
298 308
299 309 def test_copy_to_the_same_project
300 310 issue = Issue.find(1)
301 311 copy = nil
302 312 assert_difference 'Issue.count' do
303 313 copy = issue.move_to(issue.project, nil, :copy => true)
304 314 end
305 315 assert_kind_of Issue, copy
306 316 assert_equal issue.project, copy.project
307 317 assert_equal "125", copy.custom_value_for(2).value
308 318 end
309 319
310 320 def test_copy_to_another_project_and_tracker
311 321 issue = Issue.find(1)
312 322 copy = nil
313 323 assert_difference 'Issue.count' do
314 324 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
315 325 end
316 326 assert_kind_of Issue, copy
317 327 assert_equal Project.find(3), copy.project
318 328 assert_equal Tracker.find(2), copy.tracker
319 329 # Custom field #2 is not associated with target tracker
320 330 assert_nil copy.custom_value_for(2)
321 331 end
322 332
323 333 def test_issue_destroy
324 334 Issue.find(1).destroy
325 335 assert_nil Issue.find_by_id(1)
326 336 assert_nil TimeEntry.find_by_issue_id(1)
327 337 end
328 338
329 339 def test_blocked
330 340 blocked_issue = Issue.find(9)
331 341 blocking_issue = Issue.find(10)
332 342
333 343 assert blocked_issue.blocked?
334 344 assert !blocking_issue.blocked?
335 345 end
336 346
337 347 def test_blocked_issues_dont_allow_closed_statuses
338 348 blocked_issue = Issue.find(9)
339 349
340 350 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
341 351 assert !allowed_statuses.empty?
342 352 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
343 353 assert closed_statuses.empty?
344 354 end
345 355
346 356 def test_unblocked_issues_allow_closed_statuses
347 357 blocking_issue = Issue.find(10)
348 358
349 359 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
350 360 assert !allowed_statuses.empty?
351 361 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
352 362 assert !closed_statuses.empty?
353 363 end
354 364
355 365 def test_overdue
356 366 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
357 367 assert !Issue.new(:due_date => Date.today).overdue?
358 368 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
359 369 assert !Issue.new(:due_date => nil).overdue?
360 370 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
361 371 end
362 372
363 373 def test_assignable_users
364 374 assert_kind_of User, Issue.find(1).assignable_users.first
365 375 end
366 376
367 377 def test_create_should_send_email_notification
368 378 ActionMailer::Base.deliveries.clear
369 379 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
370 380
371 381 assert issue.save
372 382 assert_equal 1, ActionMailer::Base.deliveries.size
373 383 end
374 384
375 385 def test_stale_issue_should_not_send_email_notification
376 386 ActionMailer::Base.deliveries.clear
377 387 issue = Issue.find(1)
378 388 stale = Issue.find(1)
379 389
380 390 issue.init_journal(User.find(1))
381 391 issue.subject = 'Subjet update'
382 392 assert issue.save
383 393 assert_equal 1, ActionMailer::Base.deliveries.size
384 394 ActionMailer::Base.deliveries.clear
385 395
386 396 stale.init_journal(User.find(1))
387 397 stale.subject = 'Another subjet update'
388 398 assert_raise ActiveRecord::StaleObjectError do
389 399 stale.save
390 400 end
391 401 assert ActionMailer::Base.deliveries.empty?
392 402 end
393 403 end
@@ -1,550 +1,560
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :projects, :enabled_modules,
22 22 :issues, :issue_statuses, :journals, :journal_details,
23 23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 24 :queries
25 25
26 26 def setup
27 27 @ecookbook = Project.find(1)
28 28 @ecookbook_sub1 = Project.find(3)
29 29 User.current = nil
30 30 end
31 31
32 32 should_validate_presence_of :name
33 33 should_validate_presence_of :identifier
34 34
35 35 should_validate_uniqueness_of :name
36 36 should_validate_uniqueness_of :identifier
37 37
38 38 context "associations" do
39 39 should_have_many :members
40 40 should_have_many :users, :through => :members
41 41 should_have_many :member_principals
42 42 should_have_many :principals, :through => :member_principals
43 43 should_have_many :enabled_modules
44 44 should_have_many :issues
45 45 should_have_many :issue_changes, :through => :issues
46 46 should_have_many :versions
47 47 should_have_many :time_entries
48 48 should_have_many :queries
49 49 should_have_many :documents
50 50 should_have_many :news
51 51 should_have_many :issue_categories
52 52 should_have_many :boards
53 53 should_have_many :changesets, :through => :repository
54 54
55 55 should_have_one :repository
56 56 should_have_one :wiki
57 57
58 58 should_have_and_belong_to_many :trackers
59 59 should_have_and_belong_to_many :issue_custom_fields
60 60 end
61 61
62 62 def test_truth
63 63 assert_kind_of Project, @ecookbook
64 64 assert_equal "eCookbook", @ecookbook.name
65 65 end
66 66
67 67 def test_update
68 68 assert_equal "eCookbook", @ecookbook.name
69 69 @ecookbook.name = "eCook"
70 70 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
71 71 @ecookbook.reload
72 72 assert_equal "eCook", @ecookbook.name
73 73 end
74 74
75 75 def test_validate_identifier
76 76 to_test = {"abc" => true,
77 77 "ab12" => true,
78 78 "ab-12" => true,
79 79 "12" => false,
80 80 "new" => false}
81 81
82 82 to_test.each do |identifier, valid|
83 83 p = Project.new
84 84 p.identifier = identifier
85 85 p.valid?
86 86 assert_equal valid, p.errors.on('identifier').nil?
87 87 end
88 88 end
89 89
90 90 def test_members_should_be_active_users
91 91 Project.all.each do |project|
92 92 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
93 93 end
94 94 end
95 95
96 96 def test_users_should_be_active_users
97 97 Project.all.each do |project|
98 98 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
99 99 end
100 100 end
101 101
102 102 def test_archive
103 103 user = @ecookbook.members.first.user
104 104 @ecookbook.archive
105 105 @ecookbook.reload
106 106
107 107 assert !@ecookbook.active?
108 108 assert !user.projects.include?(@ecookbook)
109 109 # Subproject are also archived
110 110 assert !@ecookbook.children.empty?
111 111 assert @ecookbook.descendants.active.empty?
112 112 end
113 113
114 114 def test_unarchive
115 115 user = @ecookbook.members.first.user
116 116 @ecookbook.archive
117 117 # A subproject of an archived project can not be unarchived
118 118 assert !@ecookbook_sub1.unarchive
119 119
120 120 # Unarchive project
121 121 assert @ecookbook.unarchive
122 122 @ecookbook.reload
123 123 assert @ecookbook.active?
124 124 assert user.projects.include?(@ecookbook)
125 125 # Subproject can now be unarchived
126 126 @ecookbook_sub1.reload
127 127 assert @ecookbook_sub1.unarchive
128 128 end
129 129
130 130 def test_destroy
131 131 # 2 active members
132 132 assert_equal 2, @ecookbook.members.size
133 133 # and 1 is locked
134 134 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
135 135 # some boards
136 136 assert @ecookbook.boards.any?
137 137
138 138 @ecookbook.destroy
139 139 # make sure that the project non longer exists
140 140 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
141 141 # make sure related data was removed
142 142 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
143 143 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
144 144 end
145 145
146 146 def test_move_an_orphan_project_to_a_root_project
147 147 sub = Project.find(2)
148 148 sub.set_parent! @ecookbook
149 149 assert_equal @ecookbook.id, sub.parent.id
150 150 @ecookbook.reload
151 151 assert_equal 4, @ecookbook.children.size
152 152 end
153 153
154 154 def test_move_an_orphan_project_to_a_subproject
155 155 sub = Project.find(2)
156 156 assert sub.set_parent!(@ecookbook_sub1)
157 157 end
158 158
159 159 def test_move_a_root_project_to_a_project
160 160 sub = @ecookbook
161 161 assert sub.set_parent!(Project.find(2))
162 162 end
163 163
164 164 def test_should_not_move_a_project_to_its_children
165 165 sub = @ecookbook
166 166 assert !(sub.set_parent!(Project.find(3)))
167 167 end
168 168
169 169 def test_set_parent_should_add_roots_in_alphabetical_order
170 170 ProjectCustomField.delete_all
171 171 Project.delete_all
172 172 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
173 173 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
174 174 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
175 175 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
176 176
177 177 assert_equal 4, Project.count
178 178 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
179 179 end
180 180
181 181 def test_set_parent_should_add_children_in_alphabetical_order
182 182 ProjectCustomField.delete_all
183 183 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
184 184 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
185 185 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
186 186 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
187 187 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
188 188
189 189 parent.reload
190 190 assert_equal 4, parent.children.size
191 191 assert_equal parent.children.sort_by(&:name), parent.children
192 192 end
193 193
194 194 def test_rebuild_should_sort_children_alphabetically
195 195 ProjectCustomField.delete_all
196 196 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
197 197 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
198 198 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
199 199 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
200 200 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
201 201
202 202 Project.update_all("lft = NULL, rgt = NULL")
203 203 Project.rebuild!
204 204
205 205 parent.reload
206 206 assert_equal 4, parent.children.size
207 207 assert_equal parent.children.sort_by(&:name), parent.children
208 208 end
209 209
210 210 def test_parent
211 211 p = Project.find(6).parent
212 212 assert p.is_a?(Project)
213 213 assert_equal 5, p.id
214 214 end
215 215
216 216 def test_ancestors
217 217 a = Project.find(6).ancestors
218 218 assert a.first.is_a?(Project)
219 219 assert_equal [1, 5], a.collect(&:id)
220 220 end
221 221
222 222 def test_root
223 223 r = Project.find(6).root
224 224 assert r.is_a?(Project)
225 225 assert_equal 1, r.id
226 226 end
227 227
228 228 def test_children
229 229 c = Project.find(1).children
230 230 assert c.first.is_a?(Project)
231 231 assert_equal [5, 3, 4], c.collect(&:id)
232 232 end
233 233
234 234 def test_descendants
235 235 d = Project.find(1).descendants
236 236 assert d.first.is_a?(Project)
237 237 assert_equal [5, 6, 3, 4], d.collect(&:id)
238 238 end
239 239
240 240 def test_allowed_parents_should_be_empty_for_non_member_user
241 241 Role.non_member.add_permission!(:add_project)
242 242 user = User.find(9)
243 243 assert user.memberships.empty?
244 244 User.current = user
245 245 assert Project.new.allowed_parents.empty?
246 246 end
247 247
248 248 def test_users_by_role
249 249 users_by_role = Project.find(1).users_by_role
250 250 assert_kind_of Hash, users_by_role
251 251 role = Role.find(1)
252 252 assert_kind_of Array, users_by_role[role]
253 253 assert users_by_role[role].include?(User.find(2))
254 254 end
255 255
256 256 def test_rolled_up_trackers
257 257 parent = Project.find(1)
258 258 parent.trackers = Tracker.find([1,2])
259 259 child = parent.children.find(3)
260 260
261 261 assert_equal [1, 2], parent.tracker_ids
262 262 assert_equal [2, 3], child.trackers.collect(&:id)
263 263
264 264 assert_kind_of Tracker, parent.rolled_up_trackers.first
265 265 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
266 266
267 267 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
268 268 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
269 269 end
270 270
271 271 def test_rolled_up_trackers_should_ignore_archived_subprojects
272 272 parent = Project.find(1)
273 273 parent.trackers = Tracker.find([1,2])
274 274 child = parent.children.find(3)
275 275 child.trackers = Tracker.find([1,3])
276 276 parent.children.each(&:archive)
277 277
278 278 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
279 279 end
280 280
281 281 def test_next_identifier
282 282 ProjectCustomField.delete_all
283 283 Project.create!(:name => 'last', :identifier => 'p2008040')
284 284 assert_equal 'p2008041', Project.next_identifier
285 285 end
286 286
287 287 def test_next_identifier_first_project
288 288 Project.delete_all
289 289 assert_nil Project.next_identifier
290 290 end
291 291
292 292
293 293 def test_enabled_module_names_should_not_recreate_enabled_modules
294 294 project = Project.find(1)
295 295 # Remove one module
296 296 modules = project.enabled_modules.slice(0..-2)
297 297 assert modules.any?
298 298 assert_difference 'EnabledModule.count', -1 do
299 299 project.enabled_module_names = modules.collect(&:name)
300 300 end
301 301 project.reload
302 302 # Ids should be preserved
303 303 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
304 304 end
305 305
306 306 def test_copy_from_existing_project
307 307 source_project = Project.find(1)
308 308 copied_project = Project.copy_from(1)
309 309
310 310 assert copied_project
311 311 # Cleared attributes
312 312 assert copied_project.id.blank?
313 313 assert copied_project.name.blank?
314 314 assert copied_project.identifier.blank?
315 315
316 316 # Duplicated attributes
317 317 assert_equal source_project.description, copied_project.description
318 318 assert_equal source_project.enabled_modules, copied_project.enabled_modules
319 319 assert_equal source_project.trackers, copied_project.trackers
320 320
321 321 # Default attributes
322 322 assert_equal 1, copied_project.status
323 323 end
324 324
325 325 def test_activities_should_use_the_system_activities
326 326 project = Project.find(1)
327 327 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
328 328 end
329 329
330 330
331 331 def test_activities_should_use_the_project_specific_activities
332 332 project = Project.find(1)
333 333 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
334 334 assert overridden_activity.save!
335 335
336 336 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
337 337 end
338 338
339 339 def test_activities_should_not_include_the_inactive_project_specific_activities
340 340 project = Project.find(1)
341 341 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
342 342 assert overridden_activity.save!
343 343
344 344 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
345 345 end
346 346
347 347 def test_activities_should_not_include_project_specific_activities_from_other_projects
348 348 project = Project.find(1)
349 349 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
350 350 assert overridden_activity.save!
351 351
352 352 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
353 353 end
354 354
355 355 def test_activities_should_handle_nils
356 356 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
357 357 TimeEntryActivity.delete_all
358 358
359 359 # No activities
360 360 project = Project.find(1)
361 361 assert project.activities.empty?
362 362
363 363 # No system, one overridden
364 364 assert overridden_activity.save!
365 365 project.reload
366 366 assert_equal [overridden_activity], project.activities
367 367 end
368 368
369 369 def test_activities_should_override_system_activities_with_project_activities
370 370 project = Project.find(1)
371 371 parent_activity = TimeEntryActivity.find(:first)
372 372 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
373 373 assert overridden_activity.save!
374 374
375 375 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
376 376 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
377 377 end
378 378
379 379 def test_activities_should_include_inactive_activities_if_specified
380 380 project = Project.find(1)
381 381 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
382 382 assert overridden_activity.save!
383 383
384 384 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
385 385 end
386 386
387 387 def test_close_completed_versions
388 388 Version.update_all("status = 'open'")
389 389 project = Project.find(1)
390 390 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
391 391 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
392 392 project.close_completed_versions
393 393 project.reload
394 394 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
395 395 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
396 396 end
397 397
398 398 context "Project#copy" do
399 399 setup do
400 400 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
401 401 Project.destroy_all :identifier => "copy-test"
402 402 @source_project = Project.find(2)
403 403 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
404 404 @project.trackers = @source_project.trackers
405 405 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
406 406 end
407 407
408 408 should "copy issues" do
409 @source_project.issues << Issue.generate!(:status_id => 5,
410 :subject => "copy issue status",
411 :tracker_id => 1,
412 :assigned_to_id => 2,
413 :project_id => @source_project.id)
409 414 assert @project.valid?
410 415 assert @project.issues.empty?
411 416 assert @project.copy(@source_project)
412 417
413 418 assert_equal @source_project.issues.size, @project.issues.size
414 419 @project.issues.each do |issue|
415 420 assert issue.valid?
416 421 assert ! issue.assigned_to.blank?
417 422 assert_equal @project, issue.project
418 423 end
424
425 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
426 assert copied_issue
427 assert copied_issue.status
428 assert_equal "Closed", copied_issue.status.name
419 429 end
420 430
421 431 should "change the new issues to use the copied version" do
422 432 assigned_version = Version.generate!(:name => "Assigned Issues")
423 433 @source_project.versions << assigned_version
424 434 assert_equal 1, @source_project.versions.size
425 435 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
426 436 :subject => "change the new issues to use the copied version",
427 437 :tracker_id => 1,
428 438 :project_id => @source_project.id)
429 439
430 440 assert @project.copy(@source_project)
431 441 @project.reload
432 442 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
433 443
434 444 assert copied_issue
435 445 assert copied_issue.fixed_version
436 446 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
437 447 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
438 448 end
439 449
440 450 should "copy members" do
441 451 assert @project.valid?
442 452 assert @project.members.empty?
443 453 assert @project.copy(@source_project)
444 454
445 455 assert_equal @source_project.members.size, @project.members.size
446 456 @project.members.each do |member|
447 457 assert member
448 458 assert_equal @project, member.project
449 459 end
450 460 end
451 461
452 462 should "copy project specific queries" do
453 463 assert @project.valid?
454 464 assert @project.queries.empty?
455 465 assert @project.copy(@source_project)
456 466
457 467 assert_equal @source_project.queries.size, @project.queries.size
458 468 @project.queries.each do |query|
459 469 assert query
460 470 assert_equal @project, query.project
461 471 end
462 472 end
463 473
464 474 should "copy versions" do
465 475 @source_project.versions << Version.generate!
466 476 @source_project.versions << Version.generate!
467 477
468 478 assert @project.versions.empty?
469 479 assert @project.copy(@source_project)
470 480
471 481 assert_equal @source_project.versions.size, @project.versions.size
472 482 @project.versions.each do |version|
473 483 assert version
474 484 assert_equal @project, version.project
475 485 end
476 486 end
477 487
478 488 should "copy wiki" do
479 489 assert_difference 'Wiki.count' do
480 490 assert @project.copy(@source_project)
481 491 end
482 492
483 493 assert @project.wiki
484 494 assert_not_equal @source_project.wiki, @project.wiki
485 495 assert_equal "Start page", @project.wiki.start_page
486 496 end
487 497
488 498 should "copy wiki pages and content" do
489 499 assert @project.copy(@source_project)
490 500
491 501 assert @project.wiki
492 502 assert_equal 1, @project.wiki.pages.length
493 503
494 504 @project.wiki.pages.each do |wiki_page|
495 505 assert wiki_page.content
496 506 assert !@source_project.wiki.pages.include?(wiki_page)
497 507 end
498 508 end
499 509
500 510 should "copy custom fields"
501 511
502 512 should "copy issue categories" do
503 513 assert @project.copy(@source_project)
504 514
505 515 assert_equal 2, @project.issue_categories.size
506 516 @project.issue_categories.each do |issue_category|
507 517 assert !@source_project.issue_categories.include?(issue_category)
508 518 end
509 519 end
510 520
511 521 should "copy boards" do
512 522 assert @project.copy(@source_project)
513 523
514 524 assert_equal 1, @project.boards.size
515 525 @project.boards.each do |board|
516 526 assert !@source_project.boards.include?(board)
517 527 end
518 528 end
519 529
520 530 should "change the new issues to use the copied issue categories" do
521 531 issue = Issue.find(4)
522 532 issue.update_attribute(:category_id, 3)
523 533
524 534 assert @project.copy(@source_project)
525 535
526 536 @project.issues.each do |issue|
527 537 assert issue.category
528 538 assert_equal "Stock management", issue.category.name # Same name
529 539 assert_not_equal IssueCategory.find(3), issue.category # Different record
530 540 end
531 541 end
532 542
533 543 should "limit copy with :only option" do
534 544 assert @project.members.empty?
535 545 assert @project.issue_categories.empty?
536 546 assert @source_project.issues.any?
537 547
538 548 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
539 549
540 550 assert @project.members.any?
541 551 assert @project.issue_categories.any?
542 552 assert @project.issues.empty?
543 553 end
544 554
545 555 should "copy issue relations"
546 556 should "link issue relations if cross project issue relations are valid"
547 557
548 558 end
549 559
550 560 end
General Comments 0
You need to be logged in to leave comments. Login now