@@ -81,6 +81,7 class Project < ActiveRecord::Base | |||
|
81 | 81 | # reserved words |
|
82 | 82 | validates_exclusion_of :identifier, :in => %w( new ) |
|
83 | 83 | |
|
84 | after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} | |
|
84 | 85 | before_destroy :delete_all_members |
|
85 | 86 | |
|
86 | 87 | scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } |
@@ -383,22 +384,7 class Project < ActiveRecord::Base | |||
|
383 | 384 | # Nothing to do |
|
384 | 385 | true |
|
385 | 386 | elsif p.nil? || (p.active? && move_possible?(p)) |
|
386 | # Insert the project so that target's children or root projects stay alphabetically sorted | |
|
387 | sibs = (p.nil? ? self.class.roots : p.children) | |
|
388 | to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase } | |
|
389 | if to_be_inserted_before | |
|
390 | move_to_left_of(to_be_inserted_before) | |
|
391 | elsif p.nil? | |
|
392 | if sibs.empty? | |
|
393 | # move_to_root adds the project in first (ie. left) position | |
|
394 | move_to_root | |
|
395 | else | |
|
396 | move_to_right_of(sibs.last) unless self == sibs.last | |
|
397 | end | |
|
398 | else | |
|
399 | # move_to_child_of adds the project in last (ie.right) position | |
|
400 | move_to_child_of(p) | |
|
401 | end | |
|
387 | set_or_update_position_under(p) | |
|
402 | 388 | Issue.update_versions_from_hierarchy_change(self) |
|
403 | 389 | true |
|
404 | 390 | else |
@@ -943,4 +929,28 class Project < ActiveRecord::Base | |||
|
943 | 929 | end |
|
944 | 930 | update_attribute :status, STATUS_ARCHIVED |
|
945 | 931 | end |
|
932 | ||
|
933 | def update_position_under_parent | |
|
934 | set_or_update_position_under(parent) | |
|
935 | end | |
|
936 | ||
|
937 | # Inserts/moves the project so that target's children or root projects stay alphabetically sorted | |
|
938 | def set_or_update_position_under(target_parent) | |
|
939 | sibs = (target_parent.nil? ? self.class.roots : target_parent.children) | |
|
940 | to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } | |
|
941 | ||
|
942 | if to_be_inserted_before | |
|
943 | move_to_left_of(to_be_inserted_before) | |
|
944 | elsif target_parent.nil? | |
|
945 | if sibs.empty? | |
|
946 | # move_to_root adds the project in first (ie. left) position | |
|
947 | move_to_root | |
|
948 | else | |
|
949 | move_to_right_of(sibs.last) unless self == sibs.last | |
|
950 | end | |
|
951 | else | |
|
952 | # move_to_child_of adds the project in last (ie.right) position | |
|
953 | move_to_child_of(target_parent) | |
|
954 | end | |
|
955 | end | |
|
946 | 956 | end |
@@ -19,95 +19,108 require File.expand_path('../../test_helper', __FILE__) | |||
|
19 | 19 | |
|
20 | 20 | class ProjectNestedSetTest < ActiveSupport::TestCase |
|
21 | 21 | |
|
22 | context "nested set" do | |
|
23 | setup do | |
|
22 | def setup | |
|
24 | 23 |
|
|
25 | 24 | |
|
26 |
|
|
|
27 |
|
|
|
25 | @a = Project.create!(:name => 'A', :identifier => 'projecta') | |
|
26 | @a1 = Project.create!(:name => 'A1', :identifier => 'projecta1') | |
|
28 | 27 |
|
|
29 |
|
|
|
28 | @a2 = Project.create!(:name => 'A2', :identifier => 'projecta2') | |
|
30 | 29 |
|
|
31 | 30 | |
|
32 |
|
|
|
33 |
|
|
|
31 | @b = Project.create!(:name => 'B', :identifier => 'projectb') | |
|
32 | @b1 = Project.create!(:name => 'B1', :identifier => 'projectb1') | |
|
34 | 33 |
|
|
35 |
|
|
|
34 | @b11 = Project.create!(:name => 'B11', :identifier => 'projectb11') | |
|
36 | 35 |
|
|
37 |
|
|
|
36 | @b2 = Project.create!(:name => 'B2', :identifier => 'projectb2') | |
|
38 | 37 |
|
|
39 | 38 | |
|
40 |
|
|
|
41 |
|
|
|
39 | @c = Project.create!(:name => 'C', :identifier => 'projectc') | |
|
40 | @c1 = Project.create!(:name => 'C1', :identifier => 'projectc1') | |
|
42 | 41 |
|
|
43 | 42 | |
|
44 |
|
|
|
43 | @a, @a1, @a2, @b, @b1, @b11, @b2, @c, @c1 = *(Project.all.sort_by(&:name)) | |
|
45 | 44 |
|
|
46 | 45 | |
|
47 | context "#create" do | |
|
48 | should "build valid tree" do | |
|
49 | assert_nested_set_values({ | |
|
50 | @a => [nil, 1, 6], | |
|
51 | @a1 => [@a.id, 2, 3], | |
|
52 | @a2 => [@a.id, 4, 5], | |
|
53 | @b => [nil, 7, 14], | |
|
54 | @b1 => [@b.id, 8, 11], | |
|
55 | @b11 => [@b1.id,9, 10], | |
|
56 | @b2 => [@b.id,12, 13], | |
|
57 | @c => [nil, 15, 18], | |
|
58 | @c1 => [@c.id,16, 17] | |
|
59 | }) | |
|
60 | end | |
|
46 | def test_valid_tree | |
|
47 | assert_valid_nested_set | |
|
61 | 48 |
|
|
62 | 49 | |
|
63 | context "#set_parent!" do | |
|
64 | should "keep valid tree" do | |
|
50 | def test_moving_a_child_to_a_different_parent_should_keep_valid_tree | |
|
65 | 51 |
|
|
66 |
|
|
|
52 | Project.find_by_name('B1').set_parent!(Project.find_by_name('A2')) | |
|
67 | 53 |
|
|
68 |
|
|
|
69 | @a => [nil, 1, 10], | |
|
70 | @a2 => [@a.id, 4, 9], | |
|
71 | @b1 => [@a2.id,5, 8], | |
|
72 | @b11 => [@b1.id,6, 7], | |
|
73 | @b => [nil, 11, 14], | |
|
74 | @c => [nil, 15, 18] | |
|
75 | }) | |
|
54 | assert_valid_nested_set | |
|
76 | 55 |
|
|
56 | ||
|
57 | def test_renaming_a_root_to_first_position_should_update_nested_set_order | |
|
58 | @c.name = '1' | |
|
59 | @c.save! | |
|
60 | assert_valid_nested_set | |
|
77 | 61 |
|
|
78 | 62 | |
|
79 | context "#destroy" do | |
|
80 | context "a root with children" do | |
|
81 | should "not mess up the tree" do | |
|
82 | assert_difference 'Project.count', -4 do | |
|
83 | Project.find_by_name('Project B').destroy | |
|
63 | def test_renaming_a_root_to_middle_position_should_update_nested_set_order | |
|
64 | @a.name = 'BA' | |
|
65 | @a.save! | |
|
66 | assert_valid_nested_set | |
|
84 | 67 | end |
|
85 | assert_nested_set_values({ | |
|
86 | @a => [nil, 1, 6], | |
|
87 | @a1 => [@a.id, 2, 3], | |
|
88 | @a2 => [@a.id, 4, 5], | |
|
89 | @c => [nil, 7, 10], | |
|
90 | @c1 => [@c.id, 8, 9] | |
|
91 | }) | |
|
68 | ||
|
69 | def test_renaming_a_root_to_last_position_should_update_nested_set_order | |
|
70 | @a.name = 'D' | |
|
71 | @a.save! | |
|
72 | assert_valid_nested_set | |
|
92 | 73 |
|
|
74 | ||
|
75 | def test_renaming_a_root_to_same_position_should_update_nested_set_order | |
|
76 | @c.name = 'D' | |
|
77 | @c.save! | |
|
78 | assert_valid_nested_set | |
|
93 | 79 |
|
|
94 | 80 | |
|
95 | context "a child with children" do | |
|
96 | should "not mess up the tree" do | |
|
97 | assert_difference 'Project.count', -2 do | |
|
98 | Project.find_by_name('Project B1').destroy | |
|
81 | def test_renaming_a_child_should_update_nested_set_order | |
|
82 | @a1.name = 'A3' | |
|
83 | @a1.save! | |
|
84 | assert_valid_nested_set | |
|
85 | end | |
|
86 | ||
|
87 | def test_renaming_a_child_with_child_should_update_nested_set_order | |
|
88 | @b1.name = 'B3' | |
|
89 | @b1.save! | |
|
90 | assert_valid_nested_set | |
|
91 | end | |
|
92 | ||
|
93 | def test_adding_a_root_to_first_position_should_update_nested_set_order | |
|
94 | project = Project.create!(:name => '1', :identifier => 'projectba') | |
|
95 | assert_valid_nested_set | |
|
96 | end | |
|
97 | ||
|
98 | def test_adding_a_root_to_middle_position_should_update_nested_set_order | |
|
99 | project = Project.create!(:name => 'BA', :identifier => 'projectba') | |
|
100 | assert_valid_nested_set | |
|
101 | end | |
|
102 | ||
|
103 | def test_adding_a_root_to_last_position_should_update_nested_set_order | |
|
104 | project = Project.create!(:name => 'Z', :identifier => 'projectba') | |
|
105 | assert_valid_nested_set | |
|
99 | 106 | end |
|
100 | assert_nested_set_values({ | |
|
101 | @a => [nil, 1, 6], | |
|
102 | @b => [nil, 7, 10], | |
|
103 | @b2 => [@b.id, 8, 9], | |
|
104 | @c => [nil, 11, 14] | |
|
105 | }) | |
|
107 | ||
|
108 | def test_destroying_a_root_with_children_should_keep_valid_tree | |
|
109 | assert_difference 'Project.count', -4 do | |
|
110 | Project.find_by_name('B').destroy | |
|
106 | 111 |
|
|
112 | assert_valid_nested_set | |
|
107 | 113 |
|
|
114 | ||
|
115 | def test_destroying_a_child_with_children_should_keep_valid_tree | |
|
116 | assert_difference 'Project.count', -2 do | |
|
117 | Project.find_by_name('B1').destroy | |
|
108 | 118 | end |
|
119 | assert_valid_nested_set | |
|
109 | 120 | end |
|
110 | 121 | |
|
122 | private | |
|
123 | ||
|
111 | 124 | def assert_nested_set_values(h) |
|
112 | 125 | assert Project.valid? |
|
113 | 126 | h.each do |project, expected| |
@@ -115,4 +128,40 class ProjectNestedSetTest < ActiveSupport::TestCase | |||
|
115 | 128 | assert_equal expected, [project.parent_id, project.lft, project.rgt], "Unexpected nested set values for #{project.name}" |
|
116 | 129 | end |
|
117 | 130 | end |
|
131 | ||
|
132 | def assert_valid_nested_set | |
|
133 | projects = Project.all | |
|
134 | lft_rgt = projects.map {|p| [p.lft, p.rgt]}.flatten | |
|
135 | assert_equal projects.size * 2, lft_rgt.uniq.size | |
|
136 | assert_equal 1, lft_rgt.min | |
|
137 | assert_equal projects.size * 2, lft_rgt.max | |
|
138 | ||
|
139 | projects.each do |project| | |
|
140 | # lft should always be < rgt | |
|
141 | assert project.lft < project.rgt, "lft=#{project.lft} was not < rgt=#{project.rgt} for project #{project.name}" | |
|
142 | if project.parent_id | |
|
143 | # child lft/rgt values must be greater/lower | |
|
144 | assert_not_nil project.parent, "parent was nil for project #{project.name}" | |
|
145 | assert project.lft > project.parent.lft, "lft=#{project.lft} was not > parent.lft=#{project.parent.lft} for project #{project.name}" | |
|
146 | assert project.rgt < project.parent.rgt, "rgt=#{project.rgt} was not < parent.rgt=#{project.parent.rgt} for project #{project.name}" | |
|
147 | end | |
|
148 | # no overlapping lft/rgt values | |
|
149 | overlapping = projects.detect {|other| | |
|
150 | other != project && ( | |
|
151 | (other.lft > project.lft && other.lft < project.rgt && other.rgt > project.rgt) || | |
|
152 | (other.rgt > project.lft && other.rgt < project.rgt && other.lft < project.lft) | |
|
153 | ) | |
|
154 | } | |
|
155 | assert_nil overlapping, (overlapping && "Project #{overlapping.name} (#{overlapping.lft}/#{overlapping.rgt}) overlapped #{project.name} (#{project.lft}/#{project.rgt})") | |
|
156 | end | |
|
157 | ||
|
158 | # root projects sorted alphabetically | |
|
159 | assert_equal Project.roots.map(&:name).sort, Project.roots.sort_by(&:lft).map(&:name), "Root projects were not properly sorted" | |
|
160 | projects.each do |project| | |
|
161 | if project.children.any? | |
|
162 | # sibling projects sorted alphabetically | |
|
163 | assert_equal project.children.map(&:name).sort, project.children.order('lft').map(&:name), "Project #{project.name}'s children were not properly sorted" | |
|
164 | end | |
|
165 | end | |
|
166 | end | |
|
118 | 167 | end |
General Comments 0
You need to be logged in to leave comments.
Login now