@@ -81,6 +81,7 class Project < ActiveRecord::Base | |||||
81 | # reserved words |
|
81 | # reserved words | |
82 | validates_exclusion_of :identifier, :in => %w( new ) |
|
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 | before_destroy :delete_all_members |
|
85 | before_destroy :delete_all_members | |
85 |
|
86 | |||
86 | 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] } } |
|
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 | # Nothing to do |
|
384 | # Nothing to do | |
384 | true |
|
385 | true | |
385 | elsif p.nil? || (p.active? && move_possible?(p)) |
|
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 | set_or_update_position_under(p) | |
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 |
|
|||
402 | Issue.update_versions_from_hierarchy_change(self) |
|
388 | Issue.update_versions_from_hierarchy_change(self) | |
403 | true |
|
389 | true | |
404 | else |
|
390 | else | |
@@ -943,4 +929,28 class Project < ActiveRecord::Base | |||||
943 | end |
|
929 | end | |
944 | update_attribute :status, STATUS_ARCHIVED |
|
930 | update_attribute :status, STATUS_ARCHIVED | |
945 | end |
|
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 | end |
|
956 | end |
@@ -19,95 +19,108 require File.expand_path('../../test_helper', __FILE__) | |||||
19 |
|
19 | |||
20 | class ProjectNestedSetTest < ActiveSupport::TestCase |
|
20 | class ProjectNestedSetTest < ActiveSupport::TestCase | |
21 |
|
21 | |||
22 | context "nested set" do |
|
22 | def setup | |
23 | setup do |
|
23 | Project.delete_all | |
24 | Project.delete_all |
|
|||
25 |
|
||||
26 | @a = Project.create!(:name => 'Project A', :identifier => 'projecta') |
|
|||
27 | @a1 = Project.create!(:name => 'Project A1', :identifier => 'projecta1') |
|
|||
28 | @a1.set_parent!(@a) |
|
|||
29 | @a2 = Project.create!(:name => 'Project A2', :identifier => 'projecta2') |
|
|||
30 | @a2.set_parent!(@a) |
|
|||
31 |
|
||||
32 | @b = Project.create!(:name => 'Project B', :identifier => 'projectb') |
|
|||
33 | @b1 = Project.create!(:name => 'Project B1', :identifier => 'projectb1') |
|
|||
34 | @b1.set_parent!(@b) |
|
|||
35 | @b11 = Project.create!(:name => 'Project B11', :identifier => 'projectb11') |
|
|||
36 | @b11.set_parent!(@b1) |
|
|||
37 | @b2 = Project.create!(:name => 'Project B2', :identifier => 'projectb2') |
|
|||
38 | @b2.set_parent!(@b) |
|
|||
39 |
|
||||
40 | @c = Project.create!(:name => 'Project C', :identifier => 'projectc') |
|
|||
41 | @c1 = Project.create!(:name => 'Project C1', :identifier => 'projectc1') |
|
|||
42 | @c1.set_parent!(@c) |
|
|||
43 |
|
||||
44 | [@a, @a1, @a2, @b, @b1, @b11, @b2, @c, @c1].each(&:reload) |
|
|||
45 | end |
|
|||
46 |
|
24 | |||
47 | context "#create" do |
|
25 | @a = Project.create!(:name => 'A', :identifier => 'projecta') | |
48 | should "build valid tree" do |
|
26 | @a1 = Project.create!(:name => 'A1', :identifier => 'projecta1') | |
49 | assert_nested_set_values({ |
|
27 | @a1.set_parent!(@a) | |
50 | @a => [nil, 1, 6], |
|
28 | @a2 = Project.create!(:name => 'A2', :identifier => 'projecta2') | |
51 | @a1 => [@a.id, 2, 3], |
|
29 | @a2.set_parent!(@a) | |
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 |
|
|||
61 | end |
|
|||
62 |
|
30 | |||
63 | context "#set_parent!" do |
|
31 | @b = Project.create!(:name => 'B', :identifier => 'projectb') | |
64 | should "keep valid tree" do |
|
32 | @b1 = Project.create!(:name => 'B1', :identifier => 'projectb1') | |
65 | assert_no_difference 'Project.count' do |
|
33 | @b1.set_parent!(@b) | |
66 | Project.find_by_name('Project B1').set_parent!(Project.find_by_name('Project A2')) |
|
34 | @b11 = Project.create!(:name => 'B11', :identifier => 'projectb11') | |
67 | end |
|
35 | @b11.set_parent!(@b1) | |
68 | assert_nested_set_values({ |
|
36 | @b2 = Project.create!(:name => 'B2', :identifier => 'projectb2') | |
69 | @a => [nil, 1, 10], |
|
37 | @b2.set_parent!(@b) | |
70 | @a2 => [@a.id, 4, 9], |
|
38 | ||
71 | @b1 => [@a2.id,5, 8], |
|
39 | @c = Project.create!(:name => 'C', :identifier => 'projectc') | |
72 | @b11 => [@b1.id,6, 7], |
|
40 | @c1 = Project.create!(:name => 'C1', :identifier => 'projectc1') | |
73 | @b => [nil, 11, 14], |
|
41 | @c1.set_parent!(@c) | |
74 | @c => [nil, 15, 18] |
|
42 | ||
75 | }) |
|
43 | @a, @a1, @a2, @b, @b1, @b11, @b2, @c, @c1 = *(Project.all.sort_by(&:name)) | |
76 |
|
|
44 | end | |
|
45 | ||||
|
46 | def test_valid_tree | |||
|
47 | assert_valid_nested_set | |||
|
48 | end | |||
|
49 | ||||
|
50 | def test_moving_a_child_to_a_different_parent_should_keep_valid_tree | |||
|
51 | assert_no_difference 'Project.count' do | |||
|
52 | Project.find_by_name('B1').set_parent!(Project.find_by_name('A2')) | |||
77 | end |
|
53 | end | |
|
54 | assert_valid_nested_set | |||
|
55 | end | |||
78 |
|
56 | |||
79 | context "#destroy" do |
|
57 | def test_renaming_a_root_to_first_position_should_update_nested_set_order | |
80 | context "a root with children" do |
|
58 | @c.name = '1' | |
81 | should "not mess up the tree" do |
|
59 | @c.save! | |
82 | assert_difference 'Project.count', -4 do |
|
60 | assert_valid_nested_set | |
83 | Project.find_by_name('Project B').destroy |
|
61 | end | |
84 | 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 | }) |
|
|||
92 | end |
|
|||
93 | end |
|
|||
94 |
|
62 | |||
95 | context "a child with children" do |
|
63 | def test_renaming_a_root_to_middle_position_should_update_nested_set_order | |
96 | should "not mess up the tree" do |
|
64 | @a.name = 'BA' | |
97 | assert_difference 'Project.count', -2 do |
|
65 | @a.save! | |
98 | Project.find_by_name('Project B1').destroy |
|
66 | assert_valid_nested_set | |
99 | end |
|
67 | end | |
100 | assert_nested_set_values({ |
|
68 | ||
101 | @a => [nil, 1, 6], |
|
69 | def test_renaming_a_root_to_last_position_should_update_nested_set_order | |
102 | @b => [nil, 7, 10], |
|
70 | @a.name = 'D' | |
103 | @b2 => [@b.id, 8, 9], |
|
71 | @a.save! | |
104 | @c => [nil, 11, 14] |
|
72 | assert_valid_nested_set | |
105 | }) |
|
73 | end | |
106 | end |
|
74 | ||
107 | end |
|
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 | |||
|
79 | end | |||
|
80 | ||||
|
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 | |||
|
106 | end | |||
|
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 | |||
108 | end |
|
111 | end | |
|
112 | assert_valid_nested_set | |||
109 | end |
|
113 | end | |
110 |
|
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 | |||
|
118 | end | |||
|
119 | assert_valid_nested_set | |||
|
120 | end | |||
|
121 | ||||
|
122 | private | |||
|
123 | ||||
111 | def assert_nested_set_values(h) |
|
124 | def assert_nested_set_values(h) | |
112 | assert Project.valid? |
|
125 | assert Project.valid? | |
113 | h.each do |project, expected| |
|
126 | h.each do |project, expected| | |
@@ -115,4 +128,40 class ProjectNestedSetTest < ActiveSupport::TestCase | |||||
115 | assert_equal expected, [project.parent_id, project.lft, project.rgt], "Unexpected nested set values for #{project.name}" |
|
128 | assert_equal expected, [project.parent_id, project.lft, project.rgt], "Unexpected nested set values for #{project.name}" | |
116 | end |
|
129 | end | |
117 | end |
|
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 | end |
|
167 | end |
General Comments 0
You need to be logged in to leave comments.
Login now