##// END OF EJS Templates
set_table_name deprecated....
Jean-Philippe Lang -
r9368:f2ec23d80cf4
parent child
Show More
@@ -1,841 +1,841
1 1 require 'spec_helper'
2 2
3 3 describe "AwesomeNestedSet" do
4 4 before(:all) do
5 5 self.class.fixtures :categories, :departments, :notes, :things, :brokens
6 6 end
7 7
8 8 describe "defaults" do
9 9 it "should have left_column_default" do
10 10 Default.acts_as_nested_set_options[:left_column].should == 'lft'
11 11 end
12 12
13 13 it "should have right_column_default" do
14 14 Default.acts_as_nested_set_options[:right_column].should == 'rgt'
15 15 end
16 16
17 17 it "should have parent_column_default" do
18 18 Default.acts_as_nested_set_options[:parent_column].should == 'parent_id'
19 19 end
20 20
21 21 it "should have scope_default" do
22 22 Default.acts_as_nested_set_options[:scope].should be_nil
23 23 end
24 24
25 25 it "should have left_column_name" do
26 26 Default.left_column_name.should == 'lft'
27 27 Default.new.left_column_name.should == 'lft'
28 28 RenamedColumns.left_column_name.should == 'red'
29 29 RenamedColumns.new.left_column_name.should == 'red'
30 30 end
31 31
32 32 it "should have right_column_name" do
33 33 Default.right_column_name.should == 'rgt'
34 34 Default.new.right_column_name.should == 'rgt'
35 35 RenamedColumns.right_column_name.should == 'black'
36 36 RenamedColumns.new.right_column_name.should == 'black'
37 37 end
38 38
39 39 it "should have parent_column_name" do
40 40 Default.parent_column_name.should == 'parent_id'
41 41 Default.new.parent_column_name.should == 'parent_id'
42 42 RenamedColumns.parent_column_name.should == 'mother_id'
43 43 RenamedColumns.new.parent_column_name.should == 'mother_id'
44 44 end
45 45 end
46 46
47 47 it "creation_with_altered_column_names" do
48 48 lambda {
49 49 RenamedColumns.create!()
50 50 }.should_not raise_exception
51 51 end
52 52
53 53 it "creation when existing record has nil left column" do
54 54 assert_nothing_raised do
55 55 Broken.create!
56 56 end
57 57 end
58 58
59 59 it "quoted_left_column_name" do
60 60 quoted = Default.connection.quote_column_name('lft')
61 61 Default.quoted_left_column_name.should == quoted
62 62 Default.new.quoted_left_column_name.should == quoted
63 63 end
64 64
65 65 it "quoted_right_column_name" do
66 66 quoted = Default.connection.quote_column_name('rgt')
67 67 Default.quoted_right_column_name.should == quoted
68 68 Default.new.quoted_right_column_name.should == quoted
69 69 end
70 70
71 71 it "left_column_protected_from_assignment" do
72 72 lambda {
73 73 Category.new.lft = 1
74 74 }.should raise_exception(ActiveRecord::ActiveRecordError)
75 75 end
76 76
77 77 it "right_column_protected_from_assignment" do
78 78 lambda {
79 79 Category.new.rgt = 1
80 80 }.should raise_exception(ActiveRecord::ActiveRecordError)
81 81 end
82 82
83 83 it "scoped_appends_id" do
84 84 ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
85 85 end
86 86
87 87 it "roots_class_method" do
88 88 Category.roots.should == Category.find_all_by_parent_id(nil)
89 89 end
90 90
91 91 it "root_class_method" do
92 92 Category.root.should == categories(:top_level)
93 93 end
94 94
95 95 it "root" do
96 96 categories(:child_3).root.should == categories(:top_level)
97 97 end
98 98
99 99 it "root?" do
100 100 categories(:top_level).root?.should be_true
101 101 categories(:top_level_2).root?.should be_true
102 102 end
103 103
104 104 it "leaves_class_method" do
105 105 Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1").should == Category.leaves
106 106 Category.leaves.count.should == 4
107 107 Category.leaves.should include(categories(:child_1))
108 108 Category.leaves.should include(categories(:child_2_1))
109 109 Category.leaves.should include(categories(:child_3))
110 110 Category.leaves.should include(categories(:top_level_2))
111 111 end
112 112
113 113 it "leaf" do
114 114 categories(:child_1).leaf?.should be_true
115 115 categories(:child_2_1).leaf?.should be_true
116 116 categories(:child_3).leaf?.should be_true
117 117 categories(:top_level_2).leaf?.should be_true
118 118
119 119 categories(:top_level).leaf?.should be_false
120 120 categories(:child_2).leaf?.should be_false
121 121 Category.new.leaf?.should be_false
122 122 end
123 123
124 124
125 125 it "parent" do
126 126 categories(:child_2_1).parent.should == categories(:child_2)
127 127 end
128 128
129 129 it "self_and_ancestors" do
130 130 child = categories(:child_2_1)
131 131 self_and_ancestors = [categories(:top_level), categories(:child_2), child]
132 132 self_and_ancestors.should == child.self_and_ancestors
133 133 end
134 134
135 135 it "ancestors" do
136 136 child = categories(:child_2_1)
137 137 ancestors = [categories(:top_level), categories(:child_2)]
138 138 ancestors.should == child.ancestors
139 139 end
140 140
141 141 it "self_and_siblings" do
142 142 child = categories(:child_2)
143 143 self_and_siblings = [categories(:child_1), child, categories(:child_3)]
144 144 self_and_siblings.should == child.self_and_siblings
145 145 lambda do
146 146 tops = [categories(:top_level), categories(:top_level_2)]
147 147 assert_equal tops, categories(:top_level).self_and_siblings
148 148 end.should_not raise_exception
149 149 end
150 150
151 151 it "siblings" do
152 152 child = categories(:child_2)
153 153 siblings = [categories(:child_1), categories(:child_3)]
154 154 siblings.should == child.siblings
155 155 end
156 156
157 157 it "leaves" do
158 158 leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3)]
159 159 categories(:top_level).leaves.should == leaves
160 160 end
161 161
162 162 it "level" do
163 163 categories(:top_level).level.should == 0
164 164 categories(:child_1).level.should == 1
165 165 categories(:child_2_1).level.should == 2
166 166 end
167 167
168 168 it "has_children?" do
169 169 categories(:child_2_1).children.empty?.should be_true
170 170 categories(:child_2).children.empty?.should be_false
171 171 categories(:top_level).children.empty?.should be_false
172 172 end
173 173
174 174 it "self_and_descendents" do
175 175 parent = categories(:top_level)
176 176 self_and_descendants = [parent, categories(:child_1), categories(:child_2),
177 177 categories(:child_2_1), categories(:child_3)]
178 178 self_and_descendants.should == parent.self_and_descendants
179 179 self_and_descendants.count.should == parent.self_and_descendants.count
180 180 end
181 181
182 182 it "descendents" do
183 183 lawyers = Category.create!(:name => "lawyers")
184 184 us = Category.create!(:name => "United States")
185 185 us.move_to_child_of(lawyers)
186 186 patent = Category.create!(:name => "Patent Law")
187 187 patent.move_to_child_of(us)
188 188 lawyers.reload
189 189
190 190 lawyers.children.size.should == 1
191 191 us.children.size.should == 1
192 192 lawyers.descendants.size.should == 2
193 193 end
194 194
195 195 it "self_and_descendents" do
196 196 parent = categories(:top_level)
197 197 descendants = [categories(:child_1), categories(:child_2),
198 198 categories(:child_2_1), categories(:child_3)]
199 199 descendants.should == parent.descendants
200 200 end
201 201
202 202 it "children" do
203 203 category = categories(:top_level)
204 204 category.children.each {|c| category.id.should == c.parent_id }
205 205 end
206 206
207 207 it "order_of_children" do
208 208 categories(:child_2).move_left
209 209 categories(:child_2).should == categories(:top_level).children[0]
210 210 categories(:child_1).should == categories(:top_level).children[1]
211 211 categories(:child_3).should == categories(:top_level).children[2]
212 212 end
213 213
214 214 it "is_or_is_ancestor_of?" do
215 215 categories(:top_level).is_or_is_ancestor_of?(categories(:child_1)).should be_true
216 216 categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
217 217 categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
218 218 categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
219 219 categories(:child_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
220 220 categories(:child_1).is_or_is_ancestor_of?(categories(:child_1)).should be_true
221 221 end
222 222
223 223 it "is_ancestor_of?" do
224 224 categories(:top_level).is_ancestor_of?(categories(:child_1)).should be_true
225 225 categories(:top_level).is_ancestor_of?(categories(:child_2_1)).should be_true
226 226 categories(:child_2).is_ancestor_of?(categories(:child_2_1)).should be_true
227 227 categories(:child_2_1).is_ancestor_of?(categories(:child_2)).should be_false
228 228 categories(:child_1).is_ancestor_of?(categories(:child_2)).should be_false
229 229 categories(:child_1).is_ancestor_of?(categories(:child_1)).should be_false
230 230 end
231 231
232 232 it "is_or_is_ancestor_of_with_scope" do
233 233 root = ScopedCategory.root
234 234 child = root.children.first
235 235 root.is_or_is_ancestor_of?(child).should be_true
236 236 child.update_attribute :organization_id, 'different'
237 237 root.is_or_is_ancestor_of?(child).should be_false
238 238 end
239 239
240 240 it "is_or_is_descendant_of?" do
241 241 categories(:child_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
242 242 categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
243 243 categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2)).should be_true
244 244 categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1)).should be_false
245 245 categories(:child_2).is_or_is_descendant_of?(categories(:child_1)).should be_false
246 246 categories(:child_1).is_or_is_descendant_of?(categories(:child_1)).should be_true
247 247 end
248 248
249 249 it "is_descendant_of?" do
250 250 categories(:child_1).is_descendant_of?(categories(:top_level)).should be_true
251 251 categories(:child_2_1).is_descendant_of?(categories(:top_level)).should be_true
252 252 categories(:child_2_1).is_descendant_of?(categories(:child_2)).should be_true
253 253 categories(:child_2).is_descendant_of?(categories(:child_2_1)).should be_false
254 254 categories(:child_2).is_descendant_of?(categories(:child_1)).should be_false
255 255 categories(:child_1).is_descendant_of?(categories(:child_1)).should be_false
256 256 end
257 257
258 258 it "is_or_is_descendant_of_with_scope" do
259 259 root = ScopedCategory.root
260 260 child = root.children.first
261 261 child.is_or_is_descendant_of?(root).should be_true
262 262 child.update_attribute :organization_id, 'different'
263 263 child.is_or_is_descendant_of?(root).should be_false
264 264 end
265 265
266 266 it "same_scope?" do
267 267 root = ScopedCategory.root
268 268 child = root.children.first
269 269 child.same_scope?(root).should be_true
270 270 child.update_attribute :organization_id, 'different'
271 271 child.same_scope?(root).should be_false
272 272 end
273 273
274 274 it "left_sibling" do
275 275 categories(:child_1).should == categories(:child_2).left_sibling
276 276 categories(:child_2).should == categories(:child_3).left_sibling
277 277 end
278 278
279 279 it "left_sibling_of_root" do
280 280 categories(:top_level).left_sibling.should be_nil
281 281 end
282 282
283 283 it "left_sibling_without_siblings" do
284 284 categories(:child_2_1).left_sibling.should be_nil
285 285 end
286 286
287 287 it "left_sibling_of_leftmost_node" do
288 288 categories(:child_1).left_sibling.should be_nil
289 289 end
290 290
291 291 it "right_sibling" do
292 292 categories(:child_3).should == categories(:child_2).right_sibling
293 293 categories(:child_2).should == categories(:child_1).right_sibling
294 294 end
295 295
296 296 it "right_sibling_of_root" do
297 297 categories(:top_level_2).should == categories(:top_level).right_sibling
298 298 categories(:top_level_2).right_sibling.should be_nil
299 299 end
300 300
301 301 it "right_sibling_without_siblings" do
302 302 categories(:child_2_1).right_sibling.should be_nil
303 303 end
304 304
305 305 it "right_sibling_of_rightmost_node" do
306 306 categories(:child_3).right_sibling.should be_nil
307 307 end
308 308
309 309 it "move_left" do
310 310 categories(:child_2).move_left
311 311 categories(:child_2).left_sibling.should be_nil
312 312 categories(:child_1).should == categories(:child_2).right_sibling
313 313 Category.valid?.should be_true
314 314 end
315 315
316 316 it "move_right" do
317 317 categories(:child_2).move_right
318 318 categories(:child_2).right_sibling.should be_nil
319 319 categories(:child_3).should == categories(:child_2).left_sibling
320 320 Category.valid?.should be_true
321 321 end
322 322
323 323 it "move_to_left_of" do
324 324 categories(:child_3).move_to_left_of(categories(:child_1))
325 325 categories(:child_3).left_sibling.should be_nil
326 326 categories(:child_1).should == categories(:child_3).right_sibling
327 327 Category.valid?.should be_true
328 328 end
329 329
330 330 it "move_to_right_of" do
331 331 categories(:child_1).move_to_right_of(categories(:child_3))
332 332 categories(:child_1).right_sibling.should be_nil
333 333 categories(:child_3).should == categories(:child_1).left_sibling
334 334 Category.valid?.should be_true
335 335 end
336 336
337 337 it "move_to_root" do
338 338 categories(:child_2).move_to_root
339 339 categories(:child_2).parent.should be_nil
340 340 categories(:child_2).level.should == 0
341 341 categories(:child_2_1).level.should == 1
342 342 categories(:child_2).left.should == 1
343 343 categories(:child_2).right.should == 4
344 344 Category.valid?.should be_true
345 345 end
346 346
347 347 it "move_to_child_of" do
348 348 categories(:child_1).move_to_child_of(categories(:child_3))
349 349 categories(:child_3).id.should == categories(:child_1).parent_id
350 350 Category.valid?.should be_true
351 351 end
352 352
353 353 it "move_to_child_of_appends_to_end" do
354 354 child = Category.create! :name => 'New Child'
355 355 child.move_to_child_of categories(:top_level)
356 356 child.should == categories(:top_level).children.last
357 357 end
358 358
359 359 it "subtree_move_to_child_of" do
360 360 categories(:child_2).left.should == 4
361 361 categories(:child_2).right.should == 7
362 362
363 363 categories(:child_1).left.should == 2
364 364 categories(:child_1).right.should == 3
365 365
366 366 categories(:child_2).move_to_child_of(categories(:child_1))
367 367 Category.valid?.should be_true
368 368 categories(:child_1).id.should == categories(:child_2).parent_id
369 369
370 370 categories(:child_2).left.should == 3
371 371 categories(:child_2).right.should == 6
372 372 categories(:child_1).left.should == 2
373 373 categories(:child_1).right.should == 7
374 374 end
375 375
376 376 it "slightly_difficult_move_to_child_of" do
377 377 categories(:top_level_2).left.should == 11
378 378 categories(:top_level_2).right.should == 12
379 379
380 380 # create a new top-level node and move single-node top-level tree inside it.
381 381 new_top = Category.create(:name => 'New Top')
382 382 new_top.left.should == 13
383 383 new_top.right.should == 14
384 384
385 385 categories(:top_level_2).move_to_child_of(new_top)
386 386
387 387 Category.valid?.should be_true
388 388 new_top.id.should == categories(:top_level_2).parent_id
389 389
390 390 categories(:top_level_2).left.should == 12
391 391 categories(:top_level_2).right.should == 13
392 392 new_top.left.should == 11
393 393 new_top.right.should == 14
394 394 end
395 395
396 396 it "difficult_move_to_child_of" do
397 397 categories(:top_level).left.should == 1
398 398 categories(:top_level).right.should == 10
399 399 categories(:child_2_1).left.should == 5
400 400 categories(:child_2_1).right.should == 6
401 401
402 402 # create a new top-level node and move an entire top-level tree inside it.
403 403 new_top = Category.create(:name => 'New Top')
404 404 categories(:top_level).move_to_child_of(new_top)
405 405 categories(:child_2_1).reload
406 406 Category.valid?.should be_true
407 407 new_top.id.should == categories(:top_level).parent_id
408 408
409 409 categories(:top_level).left.should == 4
410 410 categories(:top_level).right.should == 13
411 411 categories(:child_2_1).left.should == 8
412 412 categories(:child_2_1).right.should == 9
413 413 end
414 414
415 415 #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
416 416 it "move_to_child_more_than_once_per_parent_rebuild" do
417 417 root1 = Category.create(:name => 'Root1')
418 418 root2 = Category.create(:name => 'Root2')
419 419 root3 = Category.create(:name => 'Root3')
420 420
421 421 root2.move_to_child_of root1
422 422 root3.move_to_child_of root1
423 423
424 424 output = Category.roots.last.to_text
425 425 Category.update_all('lft = null, rgt = null')
426 426 Category.rebuild!
427 427
428 428 Category.roots.last.to_text.should == output
429 429 end
430 430
431 431 # doing move_to_child twice onto same parent from the furthest right first
432 432 it "move_to_child_more_than_once_per_parent_outside_in" do
433 433 node1 = Category.create(:name => 'Node-1')
434 434 node2 = Category.create(:name => 'Node-2')
435 435 node3 = Category.create(:name => 'Node-3')
436 436
437 437 node2.move_to_child_of node1
438 438 node3.move_to_child_of node1
439 439
440 440 output = Category.roots.last.to_text
441 441 Category.update_all('lft = null, rgt = null')
442 442 Category.rebuild!
443 443
444 444 Category.roots.last.to_text.should == output
445 445 end
446 446
447 447 it "should be able to rebuild without validating each record" do
448 448 root1 = Category.create(:name => 'Root1')
449 449 root2 = Category.create(:name => 'Root2')
450 450 root3 = Category.create(:name => 'Root3')
451 451
452 452 root2.move_to_child_of root1
453 453 root3.move_to_child_of root1
454 454
455 455 root2.name = nil
456 456 root2.save!(:validate => false)
457 457
458 458 output = Category.roots.last.to_text
459 459 Category.update_all('lft = null, rgt = null')
460 460 Category.rebuild!(false)
461 461
462 462 Category.roots.last.to_text.should == output
463 463 end
464 464
465 465 it "valid_with_null_lefts" do
466 466 Category.valid?.should be_true
467 467 Category.update_all('lft = null')
468 468 Category.valid?.should be_false
469 469 end
470 470
471 471 it "valid_with_null_rights" do
472 472 Category.valid?.should be_true
473 473 Category.update_all('rgt = null')
474 474 Category.valid?.should be_false
475 475 end
476 476
477 477 it "valid_with_missing_intermediate_node" do
478 478 # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
479 479 Category.valid?.should be_true
480 480 Category.delete(categories(:child_2).id)
481 481 Category.valid?.should be_true
482 482 end
483 483
484 484 it "valid_with_overlapping_and_rights" do
485 485 Category.valid?.should be_true
486 486 categories(:top_level_2)['lft'] = 0
487 487 categories(:top_level_2).save
488 488 Category.valid?.should be_false
489 489 end
490 490
491 491 it "rebuild" do
492 492 Category.valid?.should be_true
493 493 before_text = Category.root.to_text
494 494 Category.update_all('lft = null, rgt = null')
495 495 Category.rebuild!
496 496 Category.valid?.should be_true
497 497 before_text.should == Category.root.to_text
498 498 end
499 499
500 500 it "move_possible_for_sibling" do
501 501 categories(:child_2).move_possible?(categories(:child_1)).should be_true
502 502 end
503 503
504 504 it "move_not_possible_to_self" do
505 505 categories(:top_level).move_possible?(categories(:top_level)).should be_false
506 506 end
507 507
508 508 it "move_not_possible_to_parent" do
509 509 categories(:top_level).descendants.each do |descendant|
510 510 categories(:top_level).move_possible?(descendant).should be_false
511 511 descendant.move_possible?(categories(:top_level)).should be_true
512 512 end
513 513 end
514 514
515 515 it "is_or_is_ancestor_of?" do
516 516 [:child_1, :child_2, :child_2_1, :child_3].each do |c|
517 517 categories(:top_level).is_or_is_ancestor_of?(categories(c)).should be_true
518 518 end
519 519 categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2)).should be_false
520 520 end
521 521
522 522 it "left_and_rights_valid_with_blank_left" do
523 523 Category.left_and_rights_valid?.should be_true
524 524 categories(:child_2)[:lft] = nil
525 525 categories(:child_2).save(:validate => false)
526 526 Category.left_and_rights_valid?.should be_false
527 527 end
528 528
529 529 it "left_and_rights_valid_with_blank_right" do
530 530 Category.left_and_rights_valid?.should be_true
531 531 categories(:child_2)[:rgt] = nil
532 532 categories(:child_2).save(:validate => false)
533 533 Category.left_and_rights_valid?.should be_false
534 534 end
535 535
536 536 it "left_and_rights_valid_with_equal" do
537 537 Category.left_and_rights_valid?.should be_true
538 538 categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
539 539 categories(:top_level_2).save(:validate => false)
540 540 Category.left_and_rights_valid?.should be_false
541 541 end
542 542
543 543 it "left_and_rights_valid_with_left_equal_to_parent" do
544 544 Category.left_and_rights_valid?.should be_true
545 545 categories(:child_2)[:lft] = categories(:top_level)[:lft]
546 546 categories(:child_2).save(:validate => false)
547 547 Category.left_and_rights_valid?.should be_false
548 548 end
549 549
550 550 it "left_and_rights_valid_with_right_equal_to_parent" do
551 551 Category.left_and_rights_valid?.should be_true
552 552 categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
553 553 categories(:child_2).save(:validate => false)
554 554 Category.left_and_rights_valid?.should be_false
555 555 end
556 556
557 557 it "moving_dirty_objects_doesnt_invalidate_tree" do
558 558 r1 = Category.create :name => "Test 1"
559 559 r2 = Category.create :name => "Test 2"
560 560 r3 = Category.create :name => "Test 3"
561 561 r4 = Category.create :name => "Test 4"
562 562 nodes = [r1, r2, r3, r4]
563 563
564 564 r2.move_to_child_of(r1)
565 565 Category.valid?.should be_true
566 566
567 567 r3.move_to_child_of(r1)
568 568 Category.valid?.should be_true
569 569
570 570 r4.move_to_child_of(r2)
571 571 Category.valid?.should be_true
572 572 end
573 573
574 574 it "multi_scoped_no_duplicates_for_columns?" do
575 575 lambda {
576 576 Note.no_duplicates_for_columns?
577 577 }.should_not raise_exception
578 578 end
579 579
580 580 it "multi_scoped_all_roots_valid?" do
581 581 lambda {
582 582 Note.all_roots_valid?
583 583 }.should_not raise_exception
584 584 end
585 585
586 586 it "multi_scoped" do
587 587 note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
588 588 note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
589 589 note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
590 590
591 591 [note1, note2].should == note1.self_and_siblings
592 592 [note3].should == note3.self_and_siblings
593 593 end
594 594
595 595 it "multi_scoped_rebuild" do
596 596 root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
597 597 child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
598 598 child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
599 599
600 600 child1.move_to_child_of root
601 601 child2.move_to_child_of root
602 602
603 603 Note.update_all('lft = null, rgt = null')
604 604 Note.rebuild!
605 605
606 606 Note.roots.find_by_body('A').should == root
607 607 [child1, child2].should == Note.roots.find_by_body('A').children
608 608 end
609 609
610 610 it "same_scope_with_multi_scopes" do
611 611 lambda {
612 612 notes(:scope1).same_scope?(notes(:child_1))
613 613 }.should_not raise_exception
614 614 notes(:scope1).same_scope?(notes(:child_1)).should be_true
615 615 notes(:child_1).same_scope?(notes(:scope1)).should be_true
616 616 notes(:scope1).same_scope?(notes(:scope2)).should be_false
617 617 end
618 618
619 619 it "quoting_of_multi_scope_column_names" do
620 620 ["\"notable_id\"", "\"notable_type\""].should == Note.quoted_scope_column_names
621 621 end
622 622
623 623 it "equal_in_same_scope" do
624 624 notes(:scope1).should == notes(:scope1)
625 625 notes(:scope1).should_not == notes(:child_1)
626 626 end
627 627
628 628 it "equal_in_different_scopes" do
629 629 notes(:scope1).should_not == notes(:scope2)
630 630 end
631 631
632 632 it "delete_does_not_invalidate" do
633 633 Category.acts_as_nested_set_options[:dependent] = :delete
634 634 categories(:child_2).destroy
635 635 Category.valid?.should be_true
636 636 end
637 637
638 638 it "destroy_does_not_invalidate" do
639 639 Category.acts_as_nested_set_options[:dependent] = :destroy
640 640 categories(:child_2).destroy
641 641 Category.valid?.should be_true
642 642 end
643 643
644 644 it "destroy_multiple_times_does_not_invalidate" do
645 645 Category.acts_as_nested_set_options[:dependent] = :destroy
646 646 categories(:child_2).destroy
647 647 categories(:child_2).destroy
648 648 Category.valid?.should be_true
649 649 end
650 650
651 651 it "assigning_parent_id_on_create" do
652 652 category = Category.create!(:name => "Child", :parent_id => categories(:child_2).id)
653 653 categories(:child_2).should == category.parent
654 654 categories(:child_2).id.should == category.parent_id
655 655 category.left.should_not be_nil
656 656 category.right.should_not be_nil
657 657 Category.valid?.should be_true
658 658 end
659 659
660 660 it "assigning_parent_on_create" do
661 661 category = Category.create!(:name => "Child", :parent => categories(:child_2))
662 662 categories(:child_2).should == category.parent
663 663 categories(:child_2).id.should == category.parent_id
664 664 category.left.should_not be_nil
665 665 category.right.should_not be_nil
666 666 Category.valid?.should be_true
667 667 end
668 668
669 669 it "assigning_parent_id_to_nil_on_create" do
670 670 category = Category.create!(:name => "New Root", :parent_id => nil)
671 671 category.parent.should be_nil
672 672 category.parent_id.should be_nil
673 673 category.left.should_not be_nil
674 674 category.right.should_not be_nil
675 675 Category.valid?.should be_true
676 676 end
677 677
678 678 it "assigning_parent_id_on_update" do
679 679 category = categories(:child_2_1)
680 680 category.parent_id = categories(:child_3).id
681 681 category.save
682 682 category.reload
683 683 categories(:child_3).reload
684 684 categories(:child_3).should == category.parent
685 685 categories(:child_3).id.should == category.parent_id
686 686 Category.valid?.should be_true
687 687 end
688 688
689 689 it "assigning_parent_on_update" do
690 690 category = categories(:child_2_1)
691 691 category.parent = categories(:child_3)
692 692 category.save
693 693 category.reload
694 694 categories(:child_3).reload
695 695 categories(:child_3).should == category.parent
696 696 categories(:child_3).id.should == category.parent_id
697 697 Category.valid?.should be_true
698 698 end
699 699
700 700 it "assigning_parent_id_to_nil_on_update" do
701 701 category = categories(:child_2_1)
702 702 category.parent_id = nil
703 703 category.save
704 704 category.parent.should be_nil
705 705 category.parent_id.should be_nil
706 706 Category.valid?.should be_true
707 707 end
708 708
709 709 it "creating_child_from_parent" do
710 710 category = categories(:child_2).children.create!(:name => "Child")
711 711 categories(:child_2).should == category.parent
712 712 categories(:child_2).id.should == category.parent_id
713 713 category.left.should_not be_nil
714 714 category.right.should_not be_nil
715 715 Category.valid?.should be_true
716 716 end
717 717
718 718 def check_structure(entries, structure)
719 719 structure = structure.dup
720 720 Category.each_with_level(entries) do |category, level|
721 721 expected_level, expected_name = structure.shift
722 722 expected_name.should == category.name
723 723 expected_level.should == level
724 724 end
725 725 end
726 726
727 727 it "each_with_level" do
728 728 levels = [
729 729 [0, "Top Level"],
730 730 [1, "Child 1"],
731 731 [1, "Child 2"],
732 732 [2, "Child 2.1"],
733 733 [1, "Child 3" ]]
734 734
735 735 check_structure(Category.root.self_and_descendants, levels)
736 736
737 737 # test some deeper structures
738 738 category = Category.find_by_name("Child 1")
739 739 c1 = Category.new(:name => "Child 1.1")
740 740 c2 = Category.new(:name => "Child 1.1.1")
741 741 c3 = Category.new(:name => "Child 1.1.1.1")
742 742 c4 = Category.new(:name => "Child 1.2")
743 743 [c1, c2, c3, c4].each(&:save!)
744 744
745 745 c1.move_to_child_of(category)
746 746 c2.move_to_child_of(c1)
747 747 c3.move_to_child_of(c2)
748 748 c4.move_to_child_of(category)
749 749
750 750 levels = [
751 751 [0, "Top Level"],
752 752 [1, "Child 1"],
753 753 [2, "Child 1.1"],
754 754 [3, "Child 1.1.1"],
755 755 [4, "Child 1.1.1.1"],
756 756 [2, "Child 1.2"],
757 757 [1, "Child 2"],
758 758 [2, "Child 2.1"],
759 759 [1, "Child 3" ]]
760 760
761 761 check_structure(Category.root.self_and_descendants, levels)
762 762 end
763 763
764 764 it "should not error on a model with attr_accessible" do
765 765 model = Class.new(ActiveRecord::Base)
766 model.set_table_name 'categories'
766 model.table_name = 'categories'
767 767 model.attr_accessible :name
768 768 lambda {
769 769 model.acts_as_nested_set
770 770 model.new(:name => 'foo')
771 771 }.should_not raise_exception
772 772 end
773 773
774 774 describe "before_move_callback" do
775 775 it "should fire the callback" do
776 776 categories(:child_2).should_receive(:custom_before_move)
777 777 categories(:child_2).move_to_root
778 778 end
779 779
780 780 it "should stop move when callback returns false" do
781 781 Category.test_allows_move = false
782 782 categories(:child_3).move_to_root.should be_false
783 783 categories(:child_3).root?.should be_false
784 784 end
785 785
786 786 it "should not halt save actions" do
787 787 Category.test_allows_move = false
788 788 categories(:child_3).parent_id = nil
789 789 categories(:child_3).save.should be_true
790 790 end
791 791 end
792 792
793 793 describe "counter_cache" do
794 794
795 795 it "should allow use of a counter cache for children" do
796 796 note1 = things(:parent1)
797 797 note1.children.count.should == 2
798 798 end
799 799
800 800 it "should increment the counter cache on create" do
801 801 note1 = things(:parent1)
802 802 note1.children.count.should == 2
803 803 note1[:children_count].should == 2
804 804 note1.children.create :body => 'Child 3'
805 805 note1.children.count.should == 3
806 806 note1.reload
807 807 note1[:children_count].should == 3
808 808 end
809 809
810 810 it "should decrement the counter cache on destroy" do
811 811 note1 = things(:parent1)
812 812 note1.children.count.should == 2
813 813 note1[:children_count].should == 2
814 814 note1.children.last.destroy
815 815 note1.children.count.should == 1
816 816 note1.reload
817 817 note1[:children_count].should == 1
818 818 end
819 819 end
820 820
821 821 describe "association callbacks on children" do
822 822 it "should call the appropriate callbacks on the children :has_many association " do
823 823 root = DefaultWithCallbacks.create
824 824 root.should_not be_new_record
825 825
826 826 child = root.children.build
827 827
828 828 root.before_add.should == child
829 829 root.after_add.should == child
830 830
831 831 root.before_remove.should_not == child
832 832 root.after_remove.should_not == child
833 833
834 834 child.save.should be_true
835 835 root.children.delete(child).should be_true
836 836
837 837 root.before_remove.should == child
838 838 root.after_remove.should == child
839 839 end
840 840 end
841 841 end
@@ -1,72 +1,72
1 1 class Note < ActiveRecord::Base
2 2 acts_as_nested_set :scope => [:notable_id, :notable_type]
3 3 end
4 4
5 5 class Default < ActiveRecord::Base
6 set_table_name 'categories'
6 self.table_name = 'categories'
7 7 acts_as_nested_set
8 8 end
9 9
10 10 class ScopedCategory < ActiveRecord::Base
11 set_table_name 'categories'
11 self.table_name = 'categories'
12 12 acts_as_nested_set :scope => :organization
13 13 end
14 14
15 15 class RenamedColumns < ActiveRecord::Base
16 16 acts_as_nested_set :parent_column => 'mother_id', :left_column => 'red', :right_column => 'black'
17 17 end
18 18
19 19 class Category < ActiveRecord::Base
20 20 acts_as_nested_set
21 21
22 22 validates_presence_of :name
23 23
24 24 # Setup a callback that we can switch to true or false per-test
25 25 set_callback :move, :before, :custom_before_move
26 26 cattr_accessor :test_allows_move
27 27 @@test_allows_move = true
28 28 def custom_before_move
29 29 @@test_allows_move
30 30 end
31 31
32 32 def to_s
33 33 name
34 34 end
35 35
36 36 def recurse &block
37 37 block.call self, lambda{
38 38 self.children.each do |child|
39 39 child.recurse &block
40 40 end
41 41 }
42 42 end
43 43 end
44 44
45 45 class Thing < ActiveRecord::Base
46 46 acts_as_nested_set :counter_cache => 'children_count'
47 47 end
48 48
49 49 class DefaultWithCallbacks < ActiveRecord::Base
50 50
51 set_table_name 'categories'
51 self.table_name = 'categories'
52 52
53 53 attr_accessor :before_add, :after_add, :before_remove, :after_remove
54 54
55 55 acts_as_nested_set :before_add => :do_before_add_stuff,
56 56 :after_add => :do_after_add_stuff,
57 57 :before_remove => :do_before_remove_stuff,
58 58 :after_remove => :do_after_remove_stuff
59 59
60 60 private
61 61
62 62 [ :before_add, :after_add, :before_remove, :after_remove ].each do |hook_name|
63 63 define_method "do_#{hook_name}_stuff" do |child_node|
64 64 self.send("#{hook_name}=", child_node)
65 65 end
66 66 end
67 67
68 68 end
69 69
70 70 class Broken < ActiveRecord::Base
71 71 acts_as_nested_set
72 72 end No newline at end of file
@@ -1,603 +1,603
1 1 require File.dirname(__FILE__) + '/test_helper'
2 2
3 3 class Note < ActiveRecord::Base
4 4 acts_as_nested_set :scope => [:notable_id, :notable_type]
5 5 end
6 6
7 7 class AwesomeNestedSetTest < Test::Unit::TestCase
8 8
9 9 class Default < ActiveRecord::Base
10 10 acts_as_nested_set
11 set_table_name 'categories'
11 self.table_name = 'categories'
12 12 end
13 13 class Scoped < ActiveRecord::Base
14 14 acts_as_nested_set :scope => :organization
15 set_table_name 'categories'
15 self.table_name = 'categories'
16 16 end
17 17
18 18 def test_left_column_default
19 19 assert_equal 'lft', Default.acts_as_nested_set_options[:left_column]
20 20 end
21 21
22 22 def test_right_column_default
23 23 assert_equal 'rgt', Default.acts_as_nested_set_options[:right_column]
24 24 end
25 25
26 26 def test_parent_column_default
27 27 assert_equal 'parent_id', Default.acts_as_nested_set_options[:parent_column]
28 28 end
29 29
30 30 def test_scope_default
31 31 assert_nil Default.acts_as_nested_set_options[:scope]
32 32 end
33 33
34 34 def test_left_column_name
35 35 assert_equal 'lft', Default.left_column_name
36 36 assert_equal 'lft', Default.new.left_column_name
37 37 end
38 38
39 39 def test_right_column_name
40 40 assert_equal 'rgt', Default.right_column_name
41 41 assert_equal 'rgt', Default.new.right_column_name
42 42 end
43 43
44 44 def test_parent_column_name
45 45 assert_equal 'parent_id', Default.parent_column_name
46 46 assert_equal 'parent_id', Default.new.parent_column_name
47 47 end
48 48
49 49 def test_quoted_left_column_name
50 50 quoted = Default.connection.quote_column_name('lft')
51 51 assert_equal quoted, Default.quoted_left_column_name
52 52 assert_equal quoted, Default.new.quoted_left_column_name
53 53 end
54 54
55 55 def test_quoted_right_column_name
56 56 quoted = Default.connection.quote_column_name('rgt')
57 57 assert_equal quoted, Default.quoted_right_column_name
58 58 assert_equal quoted, Default.new.quoted_right_column_name
59 59 end
60 60
61 61 def test_left_column_protected_from_assignment
62 62 assert_raises(ActiveRecord::ActiveRecordError) { Category.new.lft = 1 }
63 63 end
64 64
65 65 def test_right_column_protected_from_assignment
66 66 assert_raises(ActiveRecord::ActiveRecordError) { Category.new.rgt = 1 }
67 67 end
68 68
69 69 def test_parent_column_protected_from_assignment
70 70 assert_raises(ActiveRecord::ActiveRecordError) { Category.new.parent_id = 1 }
71 71 end
72 72
73 73 def test_colums_protected_on_initialize
74 74 c = Category.new(:lft => 1, :rgt => 2, :parent_id => 3)
75 75 assert_nil c.lft
76 76 assert_nil c.rgt
77 77 assert_nil c.parent_id
78 78 end
79 79
80 80 def test_scoped_appends_id
81 81 assert_equal :organization_id, Scoped.acts_as_nested_set_options[:scope]
82 82 end
83 83
84 84 def test_roots_class_method
85 85 assert_equal Category.find_all_by_parent_id(nil), Category.roots
86 86 end
87 87
88 88 def test_root_class_method
89 89 assert_equal categories(:top_level), Category.root
90 90 end
91 91
92 92 def test_root
93 93 assert_equal categories(:top_level), categories(:child_3).root
94 94 end
95 95
96 96 def test_root?
97 97 assert categories(:top_level).root?
98 98 assert categories(:top_level_2).root?
99 99 end
100 100
101 101 def test_leaves_class_method
102 102 assert_equal Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1"), Category.leaves
103 103 assert_equal Category.leaves.count, 4
104 104 assert (Category.leaves.include? categories(:child_1))
105 105 assert (Category.leaves.include? categories(:child_2_1))
106 106 assert (Category.leaves.include? categories(:child_3))
107 107 assert (Category.leaves.include? categories(:top_level_2))
108 108 end
109 109
110 110 def test_leaf
111 111 assert categories(:child_1).leaf?
112 112 assert categories(:child_2_1).leaf?
113 113 assert categories(:child_3).leaf?
114 114 assert categories(:top_level_2).leaf?
115 115
116 116 assert !categories(:top_level).leaf?
117 117 assert !categories(:child_2).leaf?
118 118 end
119 119
120 120 def test_parent
121 121 assert_equal categories(:child_2), categories(:child_2_1).parent
122 122 end
123 123
124 124 def test_self_and_ancestors
125 125 child = categories(:child_2_1)
126 126 self_and_ancestors = [categories(:top_level), categories(:child_2), child]
127 127 assert_equal self_and_ancestors, child.self_and_ancestors
128 128 end
129 129
130 130 def test_ancestors
131 131 child = categories(:child_2_1)
132 132 ancestors = [categories(:top_level), categories(:child_2)]
133 133 assert_equal ancestors, child.ancestors
134 134 end
135 135
136 136 def test_self_and_siblings
137 137 child = categories(:child_2)
138 138 self_and_siblings = [categories(:child_1), child, categories(:child_3)]
139 139 assert_equal self_and_siblings, child.self_and_siblings
140 140 assert_nothing_raised do
141 141 tops = [categories(:top_level), categories(:top_level_2)]
142 142 assert_equal tops, categories(:top_level).self_and_siblings
143 143 end
144 144 end
145 145
146 146 def test_siblings
147 147 child = categories(:child_2)
148 148 siblings = [categories(:child_1), categories(:child_3)]
149 149 assert_equal siblings, child.siblings
150 150 end
151 151
152 152 def test_leaves
153 153 leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3), categories(:top_level_2)]
154 154 assert categories(:top_level).leaves, leaves
155 155 end
156 156
157 157 def test_level
158 158 assert_equal 0, categories(:top_level).level
159 159 assert_equal 1, categories(:child_1).level
160 160 assert_equal 2, categories(:child_2_1).level
161 161 end
162 162
163 163 def test_has_children?
164 164 assert categories(:child_2_1).children.empty?
165 165 assert !categories(:child_2).children.empty?
166 166 assert !categories(:top_level).children.empty?
167 167 end
168 168
169 169 def test_self_and_descendents
170 170 parent = categories(:top_level)
171 171 self_and_descendants = [parent, categories(:child_1), categories(:child_2),
172 172 categories(:child_2_1), categories(:child_3)]
173 173 assert_equal self_and_descendants, parent.self_and_descendants
174 174 assert_equal self_and_descendants, parent.self_and_descendants.count
175 175 end
176 176
177 177 def test_descendents
178 178 lawyers = Category.create!(:name => "lawyers")
179 179 us = Category.create!(:name => "United States")
180 180 us.move_to_child_of(lawyers)
181 181 patent = Category.create!(:name => "Patent Law")
182 182 patent.move_to_child_of(us)
183 183 lawyers.reload
184 184
185 185 assert_equal 1, lawyers.children.size
186 186 assert_equal 1, us.children.size
187 187 assert_equal 2, lawyers.descendants.size
188 188 end
189 189
190 190 def test_self_and_descendents
191 191 parent = categories(:top_level)
192 192 descendants = [categories(:child_1), categories(:child_2),
193 193 categories(:child_2_1), categories(:child_3)]
194 194 assert_equal descendants, parent.descendants
195 195 end
196 196
197 197 def test_children
198 198 category = categories(:top_level)
199 199 category.children.each {|c| assert_equal category.id, c.parent_id }
200 200 end
201 201
202 202 def test_is_or_is_ancestor_of?
203 203 assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_1))
204 204 assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1))
205 205 assert categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1))
206 206 assert !categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2))
207 207 assert !categories(:child_1).is_or_is_ancestor_of?(categories(:child_2))
208 208 assert categories(:child_1).is_or_is_ancestor_of?(categories(:child_1))
209 209 end
210 210
211 211 def test_is_ancestor_of?
212 212 assert categories(:top_level).is_ancestor_of?(categories(:child_1))
213 213 assert categories(:top_level).is_ancestor_of?(categories(:child_2_1))
214 214 assert categories(:child_2).is_ancestor_of?(categories(:child_2_1))
215 215 assert !categories(:child_2_1).is_ancestor_of?(categories(:child_2))
216 216 assert !categories(:child_1).is_ancestor_of?(categories(:child_2))
217 217 assert !categories(:child_1).is_ancestor_of?(categories(:child_1))
218 218 end
219 219
220 220 def test_is_or_is_ancestor_of_with_scope
221 221 root = Scoped.root
222 222 child = root.children.first
223 223 assert root.is_or_is_ancestor_of?(child)
224 224 child.update_attribute :organization_id, 'different'
225 225 assert !root.is_or_is_ancestor_of?(child)
226 226 end
227 227
228 228 def test_is_or_is_descendant_of?
229 229 assert categories(:child_1).is_or_is_descendant_of?(categories(:top_level))
230 230 assert categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level))
231 231 assert categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2))
232 232 assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1))
233 233 assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_1))
234 234 assert categories(:child_1).is_or_is_descendant_of?(categories(:child_1))
235 235 end
236 236
237 237 def test_is_descendant_of?
238 238 assert categories(:child_1).is_descendant_of?(categories(:top_level))
239 239 assert categories(:child_2_1).is_descendant_of?(categories(:top_level))
240 240 assert categories(:child_2_1).is_descendant_of?(categories(:child_2))
241 241 assert !categories(:child_2).is_descendant_of?(categories(:child_2_1))
242 242 assert !categories(:child_2).is_descendant_of?(categories(:child_1))
243 243 assert !categories(:child_1).is_descendant_of?(categories(:child_1))
244 244 end
245 245
246 246 def test_is_or_is_descendant_of_with_scope
247 247 root = Scoped.root
248 248 child = root.children.first
249 249 assert child.is_or_is_descendant_of?(root)
250 250 child.update_attribute :organization_id, 'different'
251 251 assert !child.is_or_is_descendant_of?(root)
252 252 end
253 253
254 254 def test_same_scope?
255 255 root = Scoped.root
256 256 child = root.children.first
257 257 assert child.same_scope?(root)
258 258 child.update_attribute :organization_id, 'different'
259 259 assert !child.same_scope?(root)
260 260 end
261 261
262 262 def test_left_sibling
263 263 assert_equal categories(:child_1), categories(:child_2).left_sibling
264 264 assert_equal categories(:child_2), categories(:child_3).left_sibling
265 265 end
266 266
267 267 def test_left_sibling_of_root
268 268 assert_nil categories(:top_level).left_sibling
269 269 end
270 270
271 271 def test_left_sibling_without_siblings
272 272 assert_nil categories(:child_2_1).left_sibling
273 273 end
274 274
275 275 def test_left_sibling_of_leftmost_node
276 276 assert_nil categories(:child_1).left_sibling
277 277 end
278 278
279 279 def test_right_sibling
280 280 assert_equal categories(:child_3), categories(:child_2).right_sibling
281 281 assert_equal categories(:child_2), categories(:child_1).right_sibling
282 282 end
283 283
284 284 def test_right_sibling_of_root
285 285 assert_equal categories(:top_level_2), categories(:top_level).right_sibling
286 286 assert_nil categories(:top_level_2).right_sibling
287 287 end
288 288
289 289 def test_right_sibling_without_siblings
290 290 assert_nil categories(:child_2_1).right_sibling
291 291 end
292 292
293 293 def test_right_sibling_of_rightmost_node
294 294 assert_nil categories(:child_3).right_sibling
295 295 end
296 296
297 297 def test_move_left
298 298 categories(:child_2).move_left
299 299 assert_nil categories(:child_2).left_sibling
300 300 assert_equal categories(:child_1), categories(:child_2).right_sibling
301 301 assert Category.valid?
302 302 end
303 303
304 304 def test_move_right
305 305 categories(:child_2).move_right
306 306 assert_nil categories(:child_2).right_sibling
307 307 assert_equal categories(:child_3), categories(:child_2).left_sibling
308 308 assert Category.valid?
309 309 end
310 310
311 311 def test_move_to_left_of
312 312 categories(:child_3).move_to_left_of(categories(:child_1))
313 313 assert_nil categories(:child_3).left_sibling
314 314 assert_equal categories(:child_1), categories(:child_3).right_sibling
315 315 assert Category.valid?
316 316 end
317 317
318 318 def test_move_to_right_of
319 319 categories(:child_1).move_to_right_of(categories(:child_3))
320 320 assert_nil categories(:child_1).right_sibling
321 321 assert_equal categories(:child_3), categories(:child_1).left_sibling
322 322 assert Category.valid?
323 323 end
324 324
325 325 def test_move_to_root
326 326 categories(:child_2).move_to_root
327 327 assert_nil categories(:child_2).parent
328 328 assert_equal 0, categories(:child_2).level
329 329 assert_equal 1, categories(:child_2_1).level
330 330 assert_equal 1, categories(:child_2).left
331 331 assert_equal 4, categories(:child_2).right
332 332 assert Category.valid?
333 333 end
334 334
335 335 def test_move_to_child_of
336 336 categories(:child_1).move_to_child_of(categories(:child_3))
337 337 assert_equal categories(:child_3).id, categories(:child_1).parent_id
338 338 assert Category.valid?
339 339 end
340 340
341 341 def test_move_to_child_of_appends_to_end
342 342 child = Category.create! :name => 'New Child'
343 343 child.move_to_child_of categories(:top_level)
344 344 assert_equal child, categories(:top_level).children.last
345 345 end
346 346
347 347 def test_subtree_move_to_child_of
348 348 assert_equal 4, categories(:child_2).left
349 349 assert_equal 7, categories(:child_2).right
350 350
351 351 assert_equal 2, categories(:child_1).left
352 352 assert_equal 3, categories(:child_1).right
353 353
354 354 categories(:child_2).move_to_child_of(categories(:child_1))
355 355 assert Category.valid?
356 356 assert_equal categories(:child_1).id, categories(:child_2).parent_id
357 357
358 358 assert_equal 3, categories(:child_2).left
359 359 assert_equal 6, categories(:child_2).right
360 360 assert_equal 2, categories(:child_1).left
361 361 assert_equal 7, categories(:child_1).right
362 362 end
363 363
364 364 def test_slightly_difficult_move_to_child_of
365 365 assert_equal 11, categories(:top_level_2).left
366 366 assert_equal 12, categories(:top_level_2).right
367 367
368 368 # create a new top-level node and move single-node top-level tree inside it.
369 369 new_top = Category.create(:name => 'New Top')
370 370 assert_equal 13, new_top.left
371 371 assert_equal 14, new_top.right
372 372
373 373 categories(:top_level_2).move_to_child_of(new_top)
374 374
375 375 assert Category.valid?
376 376 assert_equal new_top.id, categories(:top_level_2).parent_id
377 377
378 378 assert_equal 12, categories(:top_level_2).left
379 379 assert_equal 13, categories(:top_level_2).right
380 380 assert_equal 11, new_top.left
381 381 assert_equal 14, new_top.right
382 382 end
383 383
384 384 def test_difficult_move_to_child_of
385 385 assert_equal 1, categories(:top_level).left
386 386 assert_equal 10, categories(:top_level).right
387 387 assert_equal 5, categories(:child_2_1).left
388 388 assert_equal 6, categories(:child_2_1).right
389 389
390 390 # create a new top-level node and move an entire top-level tree inside it.
391 391 new_top = Category.create(:name => 'New Top')
392 392 categories(:top_level).move_to_child_of(new_top)
393 393 categories(:child_2_1).reload
394 394 assert Category.valid?
395 395 assert_equal new_top.id, categories(:top_level).parent_id
396 396
397 397 assert_equal 4, categories(:top_level).left
398 398 assert_equal 13, categories(:top_level).right
399 399 assert_equal 8, categories(:child_2_1).left
400 400 assert_equal 9, categories(:child_2_1).right
401 401 end
402 402
403 403 #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
404 404 def test_move_to_child_more_than_once_per_parent_rebuild
405 405 root1 = Category.create(:name => 'Root1')
406 406 root2 = Category.create(:name => 'Root2')
407 407 root3 = Category.create(:name => 'Root3')
408 408
409 409 root2.move_to_child_of root1
410 410 root3.move_to_child_of root1
411 411
412 412 output = Category.roots.last.to_text
413 413 Category.update_all('lft = null, rgt = null')
414 414 Category.rebuild!
415 415
416 416 assert_equal Category.roots.last.to_text, output
417 417 end
418 418
419 419 # doing move_to_child twice onto same parent from the furthest right first
420 420 def test_move_to_child_more_than_once_per_parent_outside_in
421 421 node1 = Category.create(:name => 'Node-1')
422 422 node2 = Category.create(:name => 'Node-2')
423 423 node3 = Category.create(:name => 'Node-3')
424 424
425 425 node2.move_to_child_of node1
426 426 node3.move_to_child_of node1
427 427
428 428 output = Category.roots.last.to_text
429 429 Category.update_all('lft = null, rgt = null')
430 430 Category.rebuild!
431 431
432 432 assert_equal Category.roots.last.to_text, output
433 433 end
434 434
435 435
436 436 def test_valid_with_null_lefts
437 437 assert Category.valid?
438 438 Category.update_all('lft = null')
439 439 assert !Category.valid?
440 440 end
441 441
442 442 def test_valid_with_null_rights
443 443 assert Category.valid?
444 444 Category.update_all('rgt = null')
445 445 assert !Category.valid?
446 446 end
447 447
448 448 def test_valid_with_missing_intermediate_node
449 449 # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
450 450 assert Category.valid?
451 451 Category.delete(categories(:child_2).id)
452 452 assert Category.valid?
453 453 end
454 454
455 455 def test_valid_with_overlapping_and_rights
456 456 assert Category.valid?
457 457 categories(:top_level_2)['lft'] = 0
458 458 categories(:top_level_2).save
459 459 assert !Category.valid?
460 460 end
461 461
462 462 def test_rebuild
463 463 assert Category.valid?
464 464 before_text = Category.root.to_text
465 465 Category.update_all('lft = null, rgt = null')
466 466 Category.rebuild!
467 467 assert Category.valid?
468 468 assert_equal before_text, Category.root.to_text
469 469 end
470 470
471 471 def test_move_possible_for_sibling
472 472 assert categories(:child_2).move_possible?(categories(:child_1))
473 473 end
474 474
475 475 def test_move_not_possible_to_self
476 476 assert !categories(:top_level).move_possible?(categories(:top_level))
477 477 end
478 478
479 479 def test_move_not_possible_to_parent
480 480 categories(:top_level).descendants.each do |descendant|
481 481 assert !categories(:top_level).move_possible?(descendant)
482 482 assert descendant.move_possible?(categories(:top_level))
483 483 end
484 484 end
485 485
486 486 def test_is_or_is_ancestor_of?
487 487 [:child_1, :child_2, :child_2_1, :child_3].each do |c|
488 488 assert categories(:top_level).is_or_is_ancestor_of?(categories(c))
489 489 end
490 490 assert !categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2))
491 491 end
492 492
493 493 def test_left_and_rights_valid_with_blank_left
494 494 assert Category.left_and_rights_valid?
495 495 categories(:child_2)[:lft] = nil
496 496 categories(:child_2).save(false)
497 497 assert !Category.left_and_rights_valid?
498 498 end
499 499
500 500 def test_left_and_rights_valid_with_blank_right
501 501 assert Category.left_and_rights_valid?
502 502 categories(:child_2)[:rgt] = nil
503 503 categories(:child_2).save(false)
504 504 assert !Category.left_and_rights_valid?
505 505 end
506 506
507 507 def test_left_and_rights_valid_with_equal
508 508 assert Category.left_and_rights_valid?
509 509 categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
510 510 categories(:top_level_2).save(false)
511 511 assert !Category.left_and_rights_valid?
512 512 end
513 513
514 514 def test_left_and_rights_valid_with_left_equal_to_parent
515 515 assert Category.left_and_rights_valid?
516 516 categories(:child_2)[:lft] = categories(:top_level)[:lft]
517 517 categories(:child_2).save(false)
518 518 assert !Category.left_and_rights_valid?
519 519 end
520 520
521 521 def test_left_and_rights_valid_with_right_equal_to_parent
522 522 assert Category.left_and_rights_valid?
523 523 categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
524 524 categories(:child_2).save(false)
525 525 assert !Category.left_and_rights_valid?
526 526 end
527 527
528 528 def test_moving_dirty_objects_doesnt_invalidate_tree
529 529 r1 = Category.create
530 530 r2 = Category.create
531 531 r3 = Category.create
532 532 r4 = Category.create
533 533 nodes = [r1, r2, r3, r4]
534 534
535 535 r2.move_to_child_of(r1)
536 536 assert Category.valid?
537 537
538 538 r3.move_to_child_of(r1)
539 539 assert Category.valid?
540 540
541 541 r4.move_to_child_of(r2)
542 542 assert Category.valid?
543 543 end
544 544
545 545 def test_multi_scoped_no_duplicates_for_columns?
546 546 assert_nothing_raised do
547 547 Note.no_duplicates_for_columns?
548 548 end
549 549 end
550 550
551 551 def test_multi_scoped_all_roots_valid?
552 552 assert_nothing_raised do
553 553 Note.all_roots_valid?
554 554 end
555 555 end
556 556
557 557 def test_multi_scoped
558 558 note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
559 559 note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
560 560 note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
561 561
562 562 assert_equal [note1, note2], note1.self_and_siblings
563 563 assert_equal [note3], note3.self_and_siblings
564 564 end
565 565
566 566 def test_multi_scoped_rebuild
567 567 root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
568 568 child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
569 569 child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
570 570
571 571 child1.move_to_child_of root
572 572 child2.move_to_child_of root
573 573
574 574 Note.update_all('lft = null, rgt = null')
575 575 Note.rebuild!
576 576
577 577 assert_equal Note.roots.find_by_body('A'), root
578 578 assert_equal [child1, child2], Note.roots.find_by_body('A').children
579 579 end
580 580
581 581 def test_same_scope_with_multi_scopes
582 582 assert_nothing_raised do
583 583 notes(:scope1).same_scope?(notes(:child_1))
584 584 end
585 585 assert notes(:scope1).same_scope?(notes(:child_1))
586 586 assert notes(:child_1).same_scope?(notes(:scope1))
587 587 assert !notes(:scope1).same_scope?(notes(:scope2))
588 588 end
589 589
590 590 def test_quoting_of_multi_scope_column_names
591 591 assert_equal ["\"notable_id\"", "\"notable_type\""], Note.quoted_scope_column_names
592 592 end
593 593
594 594 def test_equal_in_same_scope
595 595 assert_equal notes(:scope1), notes(:scope1)
596 596 assert_not_equal notes(:scope1), notes(:child_1)
597 597 end
598 598
599 599 def test_equal_in_different_scopes
600 600 assert_not_equal notes(:scope1), notes(:scope2)
601 601 end
602 602
603 603 end
@@ -1,7 +1,7
1 1 class Developer < ActiveRecord::Base
2 2 has_and_belongs_to_many :projects
3 3 end
4 4
5 5 class DeVeLoPeR < ActiveRecord::Base
6 set_table_name "developers"
6 self.table_name = "developers"
7 7 end
@@ -1,512 +1,512
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 desc 'Mantis migration script'
19 19
20 20 require 'active_record'
21 21 require 'iconv'
22 22 require 'pp'
23 23
24 24 namespace :redmine do
25 25 task :migrate_from_mantis => :environment do
26 26
27 27 module MantisMigrate
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
35 35 20 => feedback_status, # feedback
36 36 30 => DEFAULT_STATUS, # acknowledged
37 37 40 => DEFAULT_STATUS, # confirmed
38 38 50 => assigned_status, # assigned
39 39 80 => resolved_status, # resolved
40 40 90 => closed_status # closed
41 41 }
42 42
43 43 priorities = IssuePriority.all
44 44 DEFAULT_PRIORITY = priorities[2]
45 45 PRIORITY_MAPPING = {10 => priorities[1], # none
46 46 20 => priorities[1], # low
47 47 30 => priorities[2], # normal
48 48 40 => priorities[3], # high
49 49 50 => priorities[4], # urgent
50 50 60 => priorities[5] # immediate
51 51 }
52 52
53 53 TRACKER_BUG = Tracker.find_by_position(1)
54 54 TRACKER_FEATURE = Tracker.find_by_position(2)
55 55
56 56 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
57 57 manager_role = roles[0]
58 58 developer_role = roles[1]
59 59 DEFAULT_ROLE = roles.last
60 60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
61 61 25 => DEFAULT_ROLE, # reporter
62 62 40 => DEFAULT_ROLE, # updater
63 63 55 => developer_role, # developer
64 64 70 => manager_role, # manager
65 65 90 => manager_role # administrator
66 66 }
67 67
68 68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
69 69 1 => 'int', # Numeric
70 70 2 => 'int', # Float
71 71 3 => 'list', # Enumeration
72 72 4 => 'string', # Email
73 73 5 => 'bool', # Checkbox
74 74 6 => 'list', # List
75 75 7 => 'list', # Multiselection list
76 76 8 => 'date', # Date
77 77 }
78 78
79 79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
80 80 2 => IssueRelation::TYPE_RELATES, # parent of
81 81 3 => IssueRelation::TYPE_RELATES, # child of
82 82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
83 83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
84 84 }
85 85
86 86 class MantisUser < ActiveRecord::Base
87 set_table_name :mantis_user_table
87 self.table_name = :mantis_user_table
88 88
89 89 def firstname
90 90 @firstname = realname.blank? ? username : realname.split.first[0..29]
91 91 @firstname
92 92 end
93 93
94 94 def lastname
95 95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
96 96 @lastname = '-' if @lastname.blank?
97 97 @lastname
98 98 end
99 99
100 100 def email
101 101 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
102 102 !User.find_by_mail(read_attribute(:email))
103 103 @email = read_attribute(:email)
104 104 else
105 105 @email = "#{username}@foo.bar"
106 106 end
107 107 end
108 108
109 109 def username
110 110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
111 111 end
112 112 end
113 113
114 114 class MantisProject < ActiveRecord::Base
115 set_table_name :mantis_project_table
115 self.table_name = :mantis_project_table
116 116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
117 117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
118 118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
119 119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
120 120
121 121 def identifier
122 122 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
123 123 end
124 124 end
125 125
126 126 class MantisVersion < ActiveRecord::Base
127 set_table_name :mantis_project_version_table
127 self.table_name = :mantis_project_version_table
128 128
129 129 def version
130 130 read_attribute(:version)[0..29]
131 131 end
132 132
133 133 def description
134 134 read_attribute(:description)[0..254]
135 135 end
136 136 end
137 137
138 138 class MantisCategory < ActiveRecord::Base
139 set_table_name :mantis_project_category_table
139 self.table_name = :mantis_project_category_table
140 140 end
141 141
142 142 class MantisProjectUser < ActiveRecord::Base
143 set_table_name :mantis_project_user_list_table
143 self.table_name = :mantis_project_user_list_table
144 144 end
145 145
146 146 class MantisBug < ActiveRecord::Base
147 set_table_name :mantis_bug_table
147 self.table_name = :mantis_bug_table
148 148 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
149 149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
150 150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
151 151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
152 152 end
153 153
154 154 class MantisBugText < ActiveRecord::Base
155 set_table_name :mantis_bug_text_table
155 self.table_name = :mantis_bug_text_table
156 156
157 157 # Adds Mantis steps_to_reproduce and additional_information fields
158 158 # to description if any
159 159 def full_description
160 160 full_description = description
161 161 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
162 162 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
163 163 full_description
164 164 end
165 165 end
166 166
167 167 class MantisBugNote < ActiveRecord::Base
168 set_table_name :mantis_bugnote_table
168 self.table_name = :mantis_bugnote_table
169 169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
170 170 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
171 171 end
172 172
173 173 class MantisBugNoteText < ActiveRecord::Base
174 set_table_name :mantis_bugnote_text_table
174 self.table_name = :mantis_bugnote_text_table
175 175 end
176 176
177 177 class MantisBugFile < ActiveRecord::Base
178 set_table_name :mantis_bug_file_table
178 self.table_name = :mantis_bug_file_table
179 179
180 180 def size
181 181 filesize
182 182 end
183 183
184 184 def original_filename
185 185 MantisMigrate.encode(filename)
186 186 end
187 187
188 188 def content_type
189 189 file_type
190 190 end
191 191
192 192 def read(*args)
193 193 if @read_finished
194 194 nil
195 195 else
196 196 @read_finished = true
197 197 content
198 198 end
199 199 end
200 200 end
201 201
202 202 class MantisBugRelationship < ActiveRecord::Base
203 set_table_name :mantis_bug_relationship_table
203 self.table_name = :mantis_bug_relationship_table
204 204 end
205 205
206 206 class MantisBugMonitor < ActiveRecord::Base
207 set_table_name :mantis_bug_monitor_table
207 self.table_name = :mantis_bug_monitor_table
208 208 end
209 209
210 210 class MantisNews < ActiveRecord::Base
211 set_table_name :mantis_news_table
211 self.table_name = :mantis_news_table
212 212 end
213 213
214 214 class MantisCustomField < ActiveRecord::Base
215 set_table_name :mantis_custom_field_table
215 self.table_name = :mantis_custom_field_table
216 216 set_inheritance_column :none
217 217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
218 218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
219 219
220 220 def format
221 221 read_attribute :type
222 222 end
223 223
224 224 def name
225 225 read_attribute(:name)[0..29]
226 226 end
227 227 end
228 228
229 229 class MantisCustomFieldProject < ActiveRecord::Base
230 set_table_name :mantis_custom_field_project_table
230 self.table_name = :mantis_custom_field_project_table
231 231 end
232 232
233 233 class MantisCustomFieldString < ActiveRecord::Base
234 set_table_name :mantis_custom_field_string_table
234 self.table_name = :mantis_custom_field_string_table
235 235 end
236 236
237 237
238 238 def self.migrate
239 239
240 240 # Users
241 241 print "Migrating users"
242 242 User.delete_all "login <> 'admin'"
243 243 users_map = {}
244 244 users_migrated = 0
245 245 MantisUser.find(:all).each do |user|
246 246 u = User.new :firstname => encode(user.firstname),
247 247 :lastname => encode(user.lastname),
248 248 :mail => user.email,
249 249 :last_login_on => user.last_visit
250 250 u.login = user.username
251 251 u.password = 'mantis'
252 252 u.status = User::STATUS_LOCKED if user.enabled != 1
253 253 u.admin = true if user.access_level == 90
254 254 next unless u.save!
255 255 users_migrated += 1
256 256 users_map[user.id] = u.id
257 257 print '.'
258 258 end
259 259 puts
260 260
261 261 # Projects
262 262 print "Migrating projects"
263 263 Project.destroy_all
264 264 projects_map = {}
265 265 versions_map = {}
266 266 categories_map = {}
267 267 MantisProject.find(:all).each do |project|
268 268 p = Project.new :name => encode(project.name),
269 269 :description => encode(project.description)
270 270 p.identifier = project.identifier
271 271 next unless p.save
272 272 projects_map[project.id] = p.id
273 273 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
274 274 p.trackers << TRACKER_BUG
275 275 p.trackers << TRACKER_FEATURE
276 276 print '.'
277 277
278 278 # Project members
279 279 project.members.each do |member|
280 280 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
281 281 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
282 282 m.project = p
283 283 m.save
284 284 end
285 285
286 286 # Project versions
287 287 project.versions.each do |version|
288 288 v = Version.new :name => encode(version.version),
289 289 :description => encode(version.description),
290 290 :effective_date => (version.date_order ? version.date_order.to_date : nil)
291 291 v.project = p
292 292 v.save
293 293 versions_map[version.id] = v.id
294 294 end
295 295
296 296 # Project categories
297 297 project.categories.each do |category|
298 298 g = IssueCategory.new :name => category.category[0,30]
299 299 g.project = p
300 300 g.save
301 301 categories_map[category.category] = g.id
302 302 end
303 303 end
304 304 puts
305 305
306 306 # Bugs
307 307 print "Migrating bugs"
308 308 Issue.destroy_all
309 309 issues_map = {}
310 310 keep_bug_ids = (Issue.count == 0)
311 311 MantisBug.find_each(:batch_size => 200) do |bug|
312 312 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
313 313 i = Issue.new :project_id => projects_map[bug.project_id],
314 314 :subject => encode(bug.summary),
315 315 :description => encode(bug.bug_text.full_description),
316 316 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
317 317 :created_on => bug.date_submitted,
318 318 :updated_on => bug.last_updated
319 319 i.author = User.find_by_id(users_map[bug.reporter_id])
320 320 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
321 321 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
322 322 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
323 323 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
324 324 i.id = bug.id if keep_bug_ids
325 325 next unless i.save
326 326 issues_map[bug.id] = i.id
327 327 print '.'
328 328 STDOUT.flush
329 329
330 330 # Assignee
331 331 # Redmine checks that the assignee is a project member
332 332 if (bug.handler_id && users_map[bug.handler_id])
333 333 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
334 334 i.save_with_validation(false)
335 335 end
336 336
337 337 # Bug notes
338 338 bug.bug_notes.each do |note|
339 339 next unless users_map[note.reporter_id]
340 340 n = Journal.new :notes => encode(note.bug_note_text.note),
341 341 :created_on => note.date_submitted
342 342 n.user = User.find_by_id(users_map[note.reporter_id])
343 343 n.journalized = i
344 344 n.save
345 345 end
346 346
347 347 # Bug files
348 348 bug.bug_files.each do |file|
349 349 a = Attachment.new :created_on => file.date_added
350 350 a.file = file
351 351 a.author = User.find :first
352 352 a.container = i
353 353 a.save
354 354 end
355 355
356 356 # Bug monitors
357 357 bug.bug_monitors.each do |monitor|
358 358 next unless users_map[monitor.user_id]
359 359 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
360 360 end
361 361 end
362 362
363 363 # update issue id sequence if needed (postgresql)
364 364 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
365 365 puts
366 366
367 367 # Bug relationships
368 368 print "Migrating bug relations"
369 369 MantisBugRelationship.find(:all).each do |relation|
370 370 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
371 371 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
372 372 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
373 373 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
374 374 pp r unless r.save
375 375 print '.'
376 376 STDOUT.flush
377 377 end
378 378 puts
379 379
380 380 # News
381 381 print "Migrating news"
382 382 News.destroy_all
383 383 MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|
384 384 next unless projects_map[news.project_id]
385 385 n = News.new :project_id => projects_map[news.project_id],
386 386 :title => encode(news.headline[0..59]),
387 387 :description => encode(news.body),
388 388 :created_on => news.date_posted
389 389 n.author = User.find_by_id(users_map[news.poster_id])
390 390 n.save
391 391 print '.'
392 392 STDOUT.flush
393 393 end
394 394 puts
395 395
396 396 # Custom fields
397 397 print "Migrating custom fields"
398 398 IssueCustomField.destroy_all
399 399 MantisCustomField.find(:all).each do |field|
400 400 f = IssueCustomField.new :name => field.name[0..29],
401 401 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
402 402 :min_length => field.length_min,
403 403 :max_length => field.length_max,
404 404 :regexp => field.valid_regexp,
405 405 :possible_values => field.possible_values.split('|'),
406 406 :is_required => field.require_report?
407 407 next unless f.save
408 408 print '.'
409 409 STDOUT.flush
410 410 # Trackers association
411 411 f.trackers = Tracker.find :all
412 412
413 413 # Projects association
414 414 field.projects.each do |project|
415 415 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
416 416 end
417 417
418 418 # Values
419 419 field.values.each do |value|
420 420 v = CustomValue.new :custom_field_id => f.id,
421 421 :value => value.value
422 422 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
423 423 v.save
424 424 end unless f.new_record?
425 425 end
426 426 puts
427 427
428 428 puts
429 429 puts "Users: #{users_migrated}/#{MantisUser.count}"
430 430 puts "Projects: #{Project.count}/#{MantisProject.count}"
431 431 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
432 432 puts "Versions: #{Version.count}/#{MantisVersion.count}"
433 433 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
434 434 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
435 435 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
436 436 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
437 437 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
438 438 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
439 439 puts "News: #{News.count}/#{MantisNews.count}"
440 440 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
441 441 end
442 442
443 443 def self.encoding(charset)
444 444 @ic = Iconv.new('UTF-8', charset)
445 445 rescue Iconv::InvalidEncoding
446 446 return false
447 447 end
448 448
449 449 def self.establish_connection(params)
450 450 constants.each do |const|
451 451 klass = const_get(const)
452 452 next unless klass.respond_to? 'establish_connection'
453 453 klass.establish_connection params
454 454 end
455 455 end
456 456
457 457 def self.encode(text)
458 458 @ic.iconv text
459 459 rescue
460 460 text
461 461 end
462 462 end
463 463
464 464 puts
465 465 if Redmine::DefaultData::Loader.no_data?
466 466 puts "Redmine configuration need to be loaded before importing data."
467 467 puts "Please, run this first:"
468 468 puts
469 469 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
470 470 exit
471 471 end
472 472
473 473 puts "WARNING: Your Redmine data will be deleted during this process."
474 474 print "Are you sure you want to continue ? [y/N] "
475 475 STDOUT.flush
476 476 break unless STDIN.gets.match(/^y$/i)
477 477
478 478 # Default Mantis database settings
479 479 db_params = {:adapter => 'mysql',
480 480 :database => 'bugtracker',
481 481 :host => 'localhost',
482 482 :username => 'root',
483 483 :password => '' }
484 484
485 485 puts
486 486 puts "Please enter settings for your Mantis database"
487 487 [:adapter, :host, :database, :username, :password].each do |param|
488 488 print "#{param} [#{db_params[param]}]: "
489 489 value = STDIN.gets.chomp!
490 490 db_params[param] = value unless value.blank?
491 491 end
492 492
493 493 while true
494 494 print "encoding [UTF-8]: "
495 495 STDOUT.flush
496 496 encoding = STDIN.gets.chomp!
497 497 encoding = 'UTF-8' if encoding.blank?
498 498 break if MantisMigrate.encoding encoding
499 499 puts "Invalid encoding!"
500 500 end
501 501 puts
502 502
503 503 # Make sure bugs can refer bugs in other projects
504 504 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
505 505
506 506 # Turn off email notifications
507 507 Setting.notified_events = []
508 508
509 509 MantisMigrate.establish_connection db_params
510 510 MantisMigrate.migrate
511 511 end
512 512 end
@@ -1,768 +1,768
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 'active_record'
19 19 require 'iconv'
20 20 require 'pp'
21 21
22 22 namespace :redmine do
23 23 desc 'Trac migration script'
24 24 task :migrate_from_trac => :environment do
25 25
26 26 module TracMigrate
27 27 TICKET_MAP = []
28 28
29 29 DEFAULT_STATUS = IssueStatus.default
30 30 assigned_status = IssueStatus.find_by_position(2)
31 31 resolved_status = IssueStatus.find_by_position(3)
32 32 feedback_status = IssueStatus.find_by_position(4)
33 33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 35 'reopened' => feedback_status,
36 36 'assigned' => assigned_status,
37 37 'closed' => closed_status
38 38 }
39 39
40 40 priorities = IssuePriority.all
41 41 DEFAULT_PRIORITY = priorities[0]
42 42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 43 'low' => priorities[0],
44 44 'normal' => priorities[1],
45 45 'high' => priorities[2],
46 46 'highest' => priorities[3],
47 47 # ---
48 48 'trivial' => priorities[0],
49 49 'minor' => priorities[1],
50 50 'major' => priorities[2],
51 51 'critical' => priorities[3],
52 52 'blocker' => priorities[4]
53 53 }
54 54
55 55 TRACKER_BUG = Tracker.find_by_position(1)
56 56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 57 DEFAULT_TRACKER = TRACKER_BUG
58 58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 59 'enhancement' => TRACKER_FEATURE,
60 60 'task' => TRACKER_FEATURE,
61 61 'patch' =>TRACKER_FEATURE
62 62 }
63 63
64 64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
65 65 manager_role = roles[0]
66 66 developer_role = roles[1]
67 67 DEFAULT_ROLE = roles.last
68 68 ROLE_MAPPING = {'admin' => manager_role,
69 69 'developer' => developer_role
70 70 }
71 71
72 72 class ::Time
73 73 class << self
74 74 alias :real_now :now
75 75 def now
76 76 real_now - @fake_diff.to_i
77 77 end
78 78 def fake(time)
79 79 @fake_diff = real_now - time
80 80 res = yield
81 81 @fake_diff = 0
82 82 res
83 83 end
84 84 end
85 85 end
86 86
87 87 class TracComponent < ActiveRecord::Base
88 set_table_name :component
88 self.table_name = :component
89 89 end
90 90
91 91 class TracMilestone < ActiveRecord::Base
92 set_table_name :milestone
92 self.table_name = :milestone
93 93 # If this attribute is set a milestone has a defined target timepoint
94 94 def due
95 95 if read_attribute(:due) && read_attribute(:due) > 0
96 96 Time.at(read_attribute(:due)).to_date
97 97 else
98 98 nil
99 99 end
100 100 end
101 101 # This is the real timepoint at which the milestone has finished.
102 102 def completed
103 103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 104 Time.at(read_attribute(:completed)).to_date
105 105 else
106 106 nil
107 107 end
108 108 end
109 109
110 110 def description
111 111 # Attribute is named descr in Trac v0.8.x
112 112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 113 end
114 114 end
115 115
116 116 class TracTicketCustom < ActiveRecord::Base
117 set_table_name :ticket_custom
117 self.table_name = :ticket_custom
118 118 end
119 119
120 120 class TracAttachment < ActiveRecord::Base
121 set_table_name :attachment
121 self.table_name = :attachment
122 122 set_inheritance_column :none
123 123
124 124 def time; Time.at(read_attribute(:time)) end
125 125
126 126 def original_filename
127 127 filename
128 128 end
129 129
130 130 def content_type
131 131 ''
132 132 end
133 133
134 134 def exist?
135 135 File.file? trac_fullpath
136 136 end
137 137
138 138 def open
139 139 File.open("#{trac_fullpath}", 'rb') {|f|
140 140 @file = f
141 141 yield self
142 142 }
143 143 end
144 144
145 145 def read(*args)
146 146 @file.read(*args)
147 147 end
148 148
149 149 def description
150 150 read_attribute(:description).to_s.slice(0,255)
151 151 end
152 152
153 153 private
154 154 def trac_fullpath
155 155 attachment_type = read_attribute(:type)
156 156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157 157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158 158 end
159 159 end
160 160
161 161 class TracTicket < ActiveRecord::Base
162 set_table_name :ticket
162 self.table_name = :ticket
163 163 set_inheritance_column :none
164 164
165 165 # ticket changes: only migrate status changes and comments
166 166 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167 167 has_many :attachments, :class_name => "TracAttachment",
168 168 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
169 169 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
170 170 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
171 171 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
172 172
173 173 def ticket_type
174 174 read_attribute(:type)
175 175 end
176 176
177 177 def summary
178 178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179 179 end
180 180
181 181 def description
182 182 read_attribute(:description).blank? ? summary : read_attribute(:description)
183 183 end
184 184
185 185 def time; Time.at(read_attribute(:time)) end
186 186 def changetime; Time.at(read_attribute(:changetime)) end
187 187 end
188 188
189 189 class TracTicketChange < ActiveRecord::Base
190 set_table_name :ticket_change
190 self.table_name = :ticket_change
191 191
192 192 def time; Time.at(read_attribute(:time)) end
193 193 end
194 194
195 195 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
196 196 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
197 197 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
198 198 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
199 199 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
200 200 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
201 201 CamelCase TitleIndex)
202 202
203 203 class TracWikiPage < ActiveRecord::Base
204 set_table_name :wiki
204 self.table_name = :wiki
205 205 set_primary_key :name
206 206
207 207 has_many :attachments, :class_name => "TracAttachment",
208 208 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
209 209 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
210 210 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
211 211
212 212 def self.columns
213 213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214 214 super.select {|column| column.name.to_s != 'readonly'}
215 215 end
216 216
217 217 def time; Time.at(read_attribute(:time)) end
218 218 end
219 219
220 220 class TracPermission < ActiveRecord::Base
221 set_table_name :permission
221 self.table_name = :permission
222 222 end
223 223
224 224 class TracSessionAttribute < ActiveRecord::Base
225 set_table_name :session_attribute
225 self.table_name = :session_attribute
226 226 end
227 227
228 228 def self.find_or_create_user(username, project_member = false)
229 229 return User.anonymous if username.blank?
230 230
231 231 u = User.find_by_login(username)
232 232 if !u
233 233 # Create a new user if not found
234 234 mail = username[0, User::MAIL_LENGTH_LIMIT]
235 235 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
236 236 mail = mail_attr.value
237 237 end
238 238 mail = "#{mail}@foo.bar" unless mail.include?("@")
239 239
240 240 name = username
241 241 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
242 242 name = name_attr.value
243 243 end
244 244 name =~ (/(.*)(\s+\w+)?/)
245 245 fn = $1.strip
246 246 ln = ($2 || '-').strip
247 247
248 248 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
249 249 :firstname => fn[0, limit_for(User, 'firstname')],
250 250 :lastname => ln[0, limit_for(User, 'lastname')]
251 251
252 252 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
253 253 u.password = 'trac'
254 254 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
255 255 # finally, a default user is used if the new user is not valid
256 256 u = User.find(:first) unless u.save
257 257 end
258 258 # Make sure he is a member of the project
259 259 if project_member && !u.member_of?(@target_project)
260 260 role = DEFAULT_ROLE
261 261 if u.admin
262 262 role = ROLE_MAPPING['admin']
263 263 elsif TracPermission.find_by_username_and_action(username, 'developer')
264 264 role = ROLE_MAPPING['developer']
265 265 end
266 266 Member.create(:user => u, :project => @target_project, :roles => [role])
267 267 u.reload
268 268 end
269 269 u
270 270 end
271 271
272 272 # Basic wiki syntax conversion
273 273 def self.convert_wiki_text(text)
274 274 # Titles
275 275 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
276 276 # External Links
277 277 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
278 278 # Ticket links:
279 279 # [ticket:234 Text],[ticket:234 This is a test]
280 280 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
281 281 # ticket:1234
282 282 # #1 is working cause Redmine uses the same syntax.
283 283 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
284 284 # Milestone links:
285 285 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
286 286 # The text "Milestone 0.1.0 (Mercury)" is not converted,
287 287 # cause Redmine's wiki does not support this.
288 288 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
289 289 # [milestone:"0.1.0 Mercury"]
290 290 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
291 291 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
292 292 # milestone:0.1.0
293 293 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
294 294 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
295 295 # Internal Links
296 296 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
297 297 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
298 298 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
299 299 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
300 300 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
301 301 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
302 302
303 303 # Links to pages UsingJustWikiCaps
304 304 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
305 305 # Normalize things that were supposed to not be links
306 306 # like !NotALink
307 307 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
308 308 # Revisions links
309 309 text = text.gsub(/\[(\d+)\]/, 'r\1')
310 310 # Ticket number re-writing
311 311 text = text.gsub(/#(\d+)/) do |s|
312 312 if $1.length < 10
313 313 # TICKET_MAP[$1.to_i] ||= $1
314 314 "\##{TICKET_MAP[$1.to_i] || $1}"
315 315 else
316 316 s
317 317 end
318 318 end
319 319 # We would like to convert the Code highlighting too
320 320 # This will go into the next line.
321 321 shebang_line = false
322 322 # Reguar expression for start of code
323 323 pre_re = /\{\{\{/
324 324 # Code hightlighing...
325 325 shebang_re = /^\#\!([a-z]+)/
326 326 # Regular expression for end of code
327 327 pre_end_re = /\}\}\}/
328 328
329 329 # Go through the whole text..extract it line by line
330 330 text = text.gsub(/^(.*)$/) do |line|
331 331 m_pre = pre_re.match(line)
332 332 if m_pre
333 333 line = '<pre>'
334 334 else
335 335 m_sl = shebang_re.match(line)
336 336 if m_sl
337 337 shebang_line = true
338 338 line = '<code class="' + m_sl[1] + '">'
339 339 end
340 340 m_pre_end = pre_end_re.match(line)
341 341 if m_pre_end
342 342 line = '</pre>'
343 343 if shebang_line
344 344 line = '</code>' + line
345 345 end
346 346 end
347 347 end
348 348 line
349 349 end
350 350
351 351 # Highlighting
352 352 text = text.gsub(/'''''([^\s])/, '_*\1')
353 353 text = text.gsub(/([^\s])'''''/, '\1*_')
354 354 text = text.gsub(/'''/, '*')
355 355 text = text.gsub(/''/, '_')
356 356 text = text.gsub(/__/, '+')
357 357 text = text.gsub(/~~/, '-')
358 358 text = text.gsub(/`/, '@')
359 359 text = text.gsub(/,,/, '~')
360 360 # Lists
361 361 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
362 362
363 363 text
364 364 end
365 365
366 366 def self.migrate
367 367 establish_connection
368 368
369 369 # Quick database test
370 370 TracComponent.count
371 371
372 372 migrated_components = 0
373 373 migrated_milestones = 0
374 374 migrated_tickets = 0
375 375 migrated_custom_values = 0
376 376 migrated_ticket_attachments = 0
377 377 migrated_wiki_edits = 0
378 378 migrated_wiki_attachments = 0
379 379
380 380 #Wiki system initializing...
381 381 @target_project.wiki.destroy if @target_project.wiki
382 382 @target_project.reload
383 383 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
384 384 wiki_edit_count = 0
385 385
386 386 # Components
387 387 print "Migrating components"
388 388 issues_category_map = {}
389 389 TracComponent.find(:all).each do |component|
390 390 print '.'
391 391 STDOUT.flush
392 392 c = IssueCategory.new :project => @target_project,
393 393 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
394 394 next unless c.save
395 395 issues_category_map[component.name] = c
396 396 migrated_components += 1
397 397 end
398 398 puts
399 399
400 400 # Milestones
401 401 print "Migrating milestones"
402 402 version_map = {}
403 403 TracMilestone.find(:all).each do |milestone|
404 404 print '.'
405 405 STDOUT.flush
406 406 # First we try to find the wiki page...
407 407 p = wiki.find_or_new_page(milestone.name.to_s)
408 408 p.content = WikiContent.new(:page => p) if p.new_record?
409 409 p.content.text = milestone.description.to_s
410 410 p.content.author = find_or_create_user('trac')
411 411 p.content.comments = 'Milestone'
412 412 p.save
413 413
414 414 v = Version.new :project => @target_project,
415 415 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
416 416 :description => nil,
417 417 :wiki_page_title => milestone.name.to_s,
418 418 :effective_date => milestone.completed
419 419
420 420 next unless v.save
421 421 version_map[milestone.name] = v
422 422 migrated_milestones += 1
423 423 end
424 424 puts
425 425
426 426 # Custom fields
427 427 # TODO: read trac.ini instead
428 428 print "Migrating custom fields"
429 429 custom_field_map = {}
430 430 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
431 431 print '.'
432 432 STDOUT.flush
433 433 # Redmine custom field name
434 434 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
435 435 # Find if the custom already exists in Redmine
436 436 f = IssueCustomField.find_by_name(field_name)
437 437 # Or create a new one
438 438 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
439 439 :field_format => 'string')
440 440
441 441 next if f.new_record?
442 442 f.trackers = Tracker.find(:all)
443 443 f.projects << @target_project
444 444 custom_field_map[field.name] = f
445 445 end
446 446 puts
447 447
448 448 # Trac 'resolution' field as a Redmine custom field
449 449 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
450 450 r = IssueCustomField.new(:name => 'Resolution',
451 451 :field_format => 'list',
452 452 :is_filter => true) if r.nil?
453 453 r.trackers = Tracker.find(:all)
454 454 r.projects << @target_project
455 455 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
456 456 r.save!
457 457 custom_field_map['resolution'] = r
458 458
459 459 # Tickets
460 460 print "Migrating tickets"
461 461 TracTicket.find_each(:batch_size => 200) do |ticket|
462 462 print '.'
463 463 STDOUT.flush
464 464 i = Issue.new :project => @target_project,
465 465 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
466 466 :description => convert_wiki_text(encode(ticket.description)),
467 467 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
468 468 :created_on => ticket.time
469 469 i.author = find_or_create_user(ticket.reporter)
470 470 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
471 471 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
472 472 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
473 473 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
474 474 i.id = ticket.id unless Issue.exists?(ticket.id)
475 475 next unless Time.fake(ticket.changetime) { i.save }
476 476 TICKET_MAP[ticket.id] = i.id
477 477 migrated_tickets += 1
478 478
479 479 # Owner
480 480 unless ticket.owner.blank?
481 481 i.assigned_to = find_or_create_user(ticket.owner, true)
482 482 Time.fake(ticket.changetime) { i.save }
483 483 end
484 484
485 485 # Comments and status/resolution changes
486 486 ticket.changes.group_by(&:time).each do |time, changeset|
487 487 status_change = changeset.select {|change| change.field == 'status'}.first
488 488 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
489 489 comment_change = changeset.select {|change| change.field == 'comment'}.first
490 490
491 491 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
492 492 :created_on => time
493 493 n.user = find_or_create_user(changeset.first.author)
494 494 n.journalized = i
495 495 if status_change &&
496 496 STATUS_MAPPING[status_change.oldvalue] &&
497 497 STATUS_MAPPING[status_change.newvalue] &&
498 498 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
499 499 n.details << JournalDetail.new(:property => 'attr',
500 500 :prop_key => 'status_id',
501 501 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
502 502 :value => STATUS_MAPPING[status_change.newvalue].id)
503 503 end
504 504 if resolution_change
505 505 n.details << JournalDetail.new(:property => 'cf',
506 506 :prop_key => custom_field_map['resolution'].id,
507 507 :old_value => resolution_change.oldvalue,
508 508 :value => resolution_change.newvalue)
509 509 end
510 510 n.save unless n.details.empty? && n.notes.blank?
511 511 end
512 512
513 513 # Attachments
514 514 ticket.attachments.each do |attachment|
515 515 next unless attachment.exist?
516 516 attachment.open {
517 517 a = Attachment.new :created_on => attachment.time
518 518 a.file = attachment
519 519 a.author = find_or_create_user(attachment.author)
520 520 a.container = i
521 521 a.description = attachment.description
522 522 migrated_ticket_attachments += 1 if a.save
523 523 }
524 524 end
525 525
526 526 # Custom fields
527 527 custom_values = ticket.customs.inject({}) do |h, custom|
528 528 if custom_field = custom_field_map[custom.name]
529 529 h[custom_field.id] = custom.value
530 530 migrated_custom_values += 1
531 531 end
532 532 h
533 533 end
534 534 if custom_field_map['resolution'] && !ticket.resolution.blank?
535 535 custom_values[custom_field_map['resolution'].id] = ticket.resolution
536 536 end
537 537 i.custom_field_values = custom_values
538 538 i.save_custom_field_values
539 539 end
540 540
541 541 # update issue id sequence if needed (postgresql)
542 542 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
543 543 puts
544 544
545 545 # Wiki
546 546 print "Migrating wiki"
547 547 if wiki.save
548 548 TracWikiPage.find(:all, :order => 'name, version').each do |page|
549 549 # Do not migrate Trac manual wiki pages
550 550 next if TRAC_WIKI_PAGES.include?(page.name)
551 551 wiki_edit_count += 1
552 552 print '.'
553 553 STDOUT.flush
554 554 p = wiki.find_or_new_page(page.name)
555 555 p.content = WikiContent.new(:page => p) if p.new_record?
556 556 p.content.text = page.text
557 557 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
558 558 p.content.comments = page.comment
559 559 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
560 560
561 561 next if p.content.new_record?
562 562 migrated_wiki_edits += 1
563 563
564 564 # Attachments
565 565 page.attachments.each do |attachment|
566 566 next unless attachment.exist?
567 567 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
568 568 attachment.open {
569 569 a = Attachment.new :created_on => attachment.time
570 570 a.file = attachment
571 571 a.author = find_or_create_user(attachment.author)
572 572 a.description = attachment.description
573 573 a.container = p
574 574 migrated_wiki_attachments += 1 if a.save
575 575 }
576 576 end
577 577 end
578 578
579 579 wiki.reload
580 580 wiki.pages.each do |page|
581 581 page.content.text = convert_wiki_text(page.content.text)
582 582 Time.fake(page.content.updated_on) { page.content.save }
583 583 end
584 584 end
585 585 puts
586 586
587 587 puts
588 588 puts "Components: #{migrated_components}/#{TracComponent.count}"
589 589 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
590 590 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
591 591 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
592 592 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
593 593 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
594 594 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
595 595 end
596 596
597 597 def self.limit_for(klass, attribute)
598 598 klass.columns_hash[attribute.to_s].limit
599 599 end
600 600
601 601 def self.encoding(charset)
602 602 @ic = Iconv.new('UTF-8', charset)
603 603 rescue Iconv::InvalidEncoding
604 604 puts "Invalid encoding!"
605 605 return false
606 606 end
607 607
608 608 def self.set_trac_directory(path)
609 609 @@trac_directory = path
610 610 raise "This directory doesn't exist!" unless File.directory?(path)
611 611 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
612 612 @@trac_directory
613 613 rescue Exception => e
614 614 puts e
615 615 return false
616 616 end
617 617
618 618 def self.trac_directory
619 619 @@trac_directory
620 620 end
621 621
622 622 def self.set_trac_adapter(adapter)
623 623 return false if adapter.blank?
624 624 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
625 625 # If adapter is sqlite or sqlite3, make sure that trac.db exists
626 626 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
627 627 @@trac_adapter = adapter
628 628 rescue Exception => e
629 629 puts e
630 630 return false
631 631 end
632 632
633 633 def self.set_trac_db_host(host)
634 634 return nil if host.blank?
635 635 @@trac_db_host = host
636 636 end
637 637
638 638 def self.set_trac_db_port(port)
639 639 return nil if port.to_i == 0
640 640 @@trac_db_port = port.to_i
641 641 end
642 642
643 643 def self.set_trac_db_name(name)
644 644 return nil if name.blank?
645 645 @@trac_db_name = name
646 646 end
647 647
648 648 def self.set_trac_db_username(username)
649 649 @@trac_db_username = username
650 650 end
651 651
652 652 def self.set_trac_db_password(password)
653 653 @@trac_db_password = password
654 654 end
655 655
656 656 def self.set_trac_db_schema(schema)
657 657 @@trac_db_schema = schema
658 658 end
659 659
660 660 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
661 661
662 662 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
663 663 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
664 664
665 665 def self.target_project_identifier(identifier)
666 666 project = Project.find_by_identifier(identifier)
667 667 if !project
668 668 # create the target project
669 669 project = Project.new :name => identifier.humanize,
670 670 :description => ''
671 671 project.identifier = identifier
672 672 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
673 673 # enable issues and wiki for the created project
674 674 project.enabled_module_names = ['issue_tracking', 'wiki']
675 675 else
676 676 puts
677 677 puts "This project already exists in your Redmine database."
678 678 print "Are you sure you want to append data to this project ? [Y/n] "
679 679 STDOUT.flush
680 680 exit if STDIN.gets.match(/^n$/i)
681 681 end
682 682 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
683 683 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
684 684 @target_project = project.new_record? ? nil : project
685 685 @target_project.reload
686 686 end
687 687
688 688 def self.connection_params
689 689 if %w(sqlite sqlite3).include?(trac_adapter)
690 690 {:adapter => trac_adapter,
691 691 :database => trac_db_path}
692 692 else
693 693 {:adapter => trac_adapter,
694 694 :database => trac_db_name,
695 695 :host => trac_db_host,
696 696 :port => trac_db_port,
697 697 :username => trac_db_username,
698 698 :password => trac_db_password,
699 699 :schema_search_path => trac_db_schema
700 700 }
701 701 end
702 702 end
703 703
704 704 def self.establish_connection
705 705 constants.each do |const|
706 706 klass = const_get(const)
707 707 next unless klass.respond_to? 'establish_connection'
708 708 klass.establish_connection connection_params
709 709 end
710 710 end
711 711
712 712 private
713 713 def self.encode(text)
714 714 @ic.iconv text
715 715 rescue
716 716 text
717 717 end
718 718 end
719 719
720 720 puts
721 721 if Redmine::DefaultData::Loader.no_data?
722 722 puts "Redmine configuration need to be loaded before importing data."
723 723 puts "Please, run this first:"
724 724 puts
725 725 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
726 726 exit
727 727 end
728 728
729 729 puts "WARNING: a new project will be added to Redmine during this process."
730 730 print "Are you sure you want to continue ? [y/N] "
731 731 STDOUT.flush
732 732 break unless STDIN.gets.match(/^y$/i)
733 733 puts
734 734
735 735 def prompt(text, options = {}, &block)
736 736 default = options[:default] || ''
737 737 while true
738 738 print "#{text} [#{default}]: "
739 739 STDOUT.flush
740 740 value = STDIN.gets.chomp!
741 741 value = default if value.blank?
742 742 break if yield value
743 743 end
744 744 end
745 745
746 746 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
747 747
748 748 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
749 749 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
750 750 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
751 751 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
752 752 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
753 753 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
754 754 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
755 755 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
756 756 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
757 757 end
758 758 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
759 759 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
760 760 puts
761 761
762 762 # Turn off email notifications
763 763 Setting.notified_events = []
764 764
765 765 TracMigrate.migrate
766 766 end
767 767 end
768 768
General Comments 0
You need to be logged in to leave comments. Login now