acts_as_ordered_tree 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in acts_as_ordered_tree.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Acts As Ordered Tree
2
+ WARNING! THIS GEM IS NOT COMPATIBLE WITH <a href="http://ordered-tree.rubyforge.org">ordered_tree gem</a>.
3
+
4
+ Specify this `acts_as` extension if you want to model an ordered tree structure by providing a parent association, a children
5
+ association and a sort column. For proper use you should have a foreign key column, which by default is called `parent_id`, and
6
+ a sort column, which by default is called `position`.
7
+
8
+ ## Requirements
9
+ Gem depends on `active_record >= 3`.
10
+
11
+ ## Installation
12
+ Install it via rubygems:
13
+
14
+ ```bash
15
+ gem install acts_as_ordered_tree
16
+ ```
17
+
18
+ Gem depends on `acts_as_tree` and `acts_as_list` gems.
19
+
20
+ Setup your model:
21
+
22
+ ```ruby
23
+ class Node < ActiveRecord::Base
24
+ acts_as_ordered_tree
25
+
26
+ # gem introduces new ActiveRecord callbacks:
27
+ # *_reorder - fires when position (but not parent node) is changed
28
+ # *_move - fires when parent node is changed
29
+ before_reorder :do_smth
30
+ before_move :do_smth_else
31
+ end
32
+ ```
33
+
34
+ ## Example
35
+ ```ruby
36
+ # root
37
+ # \_ child1
38
+ # \_ subchild1
39
+ # \_ subchild2
40
+
41
+
42
+ root = Node.create(:name => "root")
43
+ child1 = root.children.create(:name => "child1")
44
+ subchild1 = child1.children.create("name" => "subchild1")
45
+ subchild2 = child1.children.create("name" => "subchild2")
46
+
47
+ Node.roots # => [root]
48
+
49
+ root.root? # => true
50
+ root.parent # => nil
51
+ root.ancestors # => []
52
+ root.descendants # => [child1, subchild1, subchild2]
53
+
54
+ child1.parent # => root
55
+ child1.ancestors # => [root]
56
+ child1.children # => [subchild1, subchild2]
57
+ child1.descendants # => [subchild1, subchild2]
58
+ child1.root? # => false
59
+ child1.leaf? # => false
60
+
61
+ subchild1.ancestors # => [child1, root]
62
+ subchild1.root # => [root]
63
+ subchild1.leaf? # => true
64
+ subchild1.first? # => true
65
+ subchild1.last? # => false
66
+ subchild2.last? # => true
67
+
68
+ subchild1.move_to_above_of(child1)
69
+ subchild1.move_to_bottom_of(child1)
70
+ subchild1.move_to_child_of(root)
71
+ subchild1.move_lower
72
+ subchild1.move_higher
73
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "acts_as_ordered_tree/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "acts_as_ordered_tree"
7
+ s.version = ActsAsOrderedTree::VERSION
8
+ s.authors = ["Alexei Mikhailov"]
9
+ s.email = ["amikhailov83@gmail.com"]
10
+ s.homepage = "https://github.com/take-five/acts_as_ordered_tree"
11
+ s.summary = %q{ActiveRecord extension for sorted adjacency lists support}
12
+
13
+ s.rubyforge_project = "acts_as_ordered_tree"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency "activesupport", "~> 3"
21
+ s.add_dependency "activerecord", "~> 3"
22
+ s.add_dependency "acts_as_tree", "~> 0.1"
23
+ s.add_dependency "acts_as_list", "~> 0.1"
24
+
25
+ s.add_development_dependency "rspec"
26
+ s.add_development_dependency "simplecov"
27
+ s.add_development_dependency "sqlite3"
28
+ s.add_development_dependency "bundler"
29
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
@@ -0,0 +1,36 @@
1
+ require "enumerator"
2
+
3
+ module ActsAsOrderedTree
4
+ # Enhanced enumerator
5
+ #
6
+ # Allows to use array specific methods like +empty?+, +reverse?+ and so on
7
+ class Iterator < Enumerator
8
+ class NullArgument < ArgumentError; end
9
+ NA = NullArgument.new
10
+
11
+ def initialize(*args, &block)
12
+ @enumerator = Enumerator.new(*args, &block)
13
+
14
+ super() do |yielder|
15
+ @enumerator.each do |e|
16
+ yielder << e
17
+ end
18
+ end
19
+ end
20
+
21
+ # Delegate everything to underlying array
22
+ def method_missing(method_id, *args, &block)
23
+ if method_id !~ /^(__|instance_eval|class|object_id)/
24
+ to_ary!.__send__(method_id, *args, &block)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ private
31
+ def to_ary!
32
+ @enumerator = @enumerator.to_a unless @enumerator.is_a?(Array)
33
+ @enumerator
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,100 @@
1
+ require "active_support/concern"
2
+ require "active_support/core_ext/object/with_options"
3
+
4
+ module ActsAsOrderedTree
5
+ module List
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include PatchedMethods
10
+ scope :ordered, order(position_column)
11
+
12
+ with_options :if => :parent_changed? do |opts|
13
+ opts.before_update :remove_from_old_list
14
+ opts.before_update :add_to_list_bottom
15
+ end
16
+
17
+ define_model_callbacks :reorder
18
+ around_update :__around_reorder, :if => :position_changed?,
19
+ :unless => :parent_changed?
20
+ end
21
+
22
+ # Returns true if record has changes in +parent_id+
23
+ def position_changed?
24
+ changes.has_key?(position_column.to_s)
25
+ end
26
+
27
+ private
28
+ # Turn off reorder callbacks temporary
29
+ def skip_reorder_callbacks(skip = true) #:nodoc:
30
+ @skip_reorder_callbacks = skip
31
+ result = yield
32
+ @skip_reorder_callbacks = false
33
+
34
+ result
35
+ end
36
+
37
+ def __around_reorder #:nodoc:
38
+ if @skip_reorder_callbacks
39
+ yield
40
+ else
41
+ run_callbacks(:reorder) { yield }
42
+ end
43
+ end
44
+
45
+ # It should invoke callbacks, so we patch +acts_as_list+ methods
46
+ module PatchedMethods #:nodoc:all
47
+ private
48
+ def remove_from_old_list
49
+ unchanged = self.class.find(id)
50
+ unchanged.send(:decrement_positions_on_lower_items)
51
+
52
+ nil
53
+ end
54
+
55
+ # This has the effect of moving all the higher items up one.
56
+ def decrement_positions_on_higher_items(position)
57
+ higher_than(position).each do |node|
58
+ node.decrement!(position_column)
59
+ end
60
+ end
61
+
62
+ # This has the effect of moving all the lower items up one.
63
+ def decrement_positions_on_lower_items
64
+ return unless in_list?
65
+ lower_than(position).each do |node|
66
+ node.decrement!(position_column)
67
+ end
68
+ end
69
+
70
+ # This has the effect of moving all the higher items down one.
71
+ def increment_positions_on_higher_items
72
+ return unless in_list?
73
+
74
+ higher_than(self[position_column]).each do |node|
75
+ node.increment!(position_column)
76
+ end
77
+ end
78
+
79
+ def increment_positions_on_all_items
80
+ self_and_siblings.each do |sib|
81
+ sib.increment!(position_column)
82
+ end
83
+ end
84
+
85
+ def increment_positions_on_lower_items(position)
86
+ lower_than(position).each do |node|
87
+ node.increment!(position_column)
88
+ end
89
+ end
90
+
91
+ def lower_than(position)
92
+ acts_as_list_class.where(scope_condition).where("#{position_column} >= ?", position.to_i)
93
+ end
94
+
95
+ def higher_than(position)
96
+ acts_as_list_class.where(scope_condition).where("#{position_column} < ?", position.to_i)
97
+ end
98
+ end
99
+ end # module List
100
+ end # module ActsAsOrderedTree
@@ -0,0 +1,156 @@
1
+ require "active_support/concern"
2
+
3
+ module ActsAsOrderedTree
4
+ module Tree
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # remove +acts_as_tree+ version of +roots+ method
9
+ class << self
10
+ remove_method :roots
11
+
12
+ # Retrieve first root node
13
+ #
14
+ # Replacement for native +ActsAsTree.root+ method
15
+ def root
16
+ roots.first
17
+ end
18
+ end
19
+
20
+ scope :roots, where(parent_column => nil).order(quoted_position_column)
21
+
22
+ validate :validate_incest
23
+
24
+ define_model_callbacks :move
25
+ around_update :__around_move, :if => :parent_changed?
26
+ end
27
+
28
+ # == Instance methods
29
+
30
+ # returns a Enumerator of ancestors, starting from parent until root
31
+ def ancestors
32
+ Iterator.new do |yielder|
33
+ node = self
34
+ yielder << node while node = node.parent
35
+ end
36
+ end
37
+
38
+ # returns a Enumerator of ancestors, including self
39
+ def self_and_ancestors
40
+ Iterator.new do |y|
41
+ y << self
42
+ ancestors.each { |a| y << a }
43
+ end
44
+ end
45
+
46
+ # returns a Enumerator of node's descendants, traversing depth first
47
+ #
48
+ # == Example
49
+ # The tree:
50
+ # # * root
51
+ # # * child_1
52
+ # # * grandchild_1_1
53
+ # # * grandchild_1_2
54
+ # # * child_2
55
+ # # * grandchild_2_1
56
+ #
57
+ # root.descendants # => [root,
58
+ # # child_1, grandchild_1_1, grandchild_1_2,
59
+ # # child_2, grandchild_2_1]
60
+ def descendants
61
+ Iterator.new do |yielder|
62
+ children.each do |child|
63
+ yielder << child
64
+
65
+ child.descendants.each do |grandchild|
66
+ yielder << grandchild
67
+ end
68
+ end
69
+ end
70
+ end # def descendants
71
+
72
+ def self_and_descendants
73
+ Iterator.new do |y|
74
+ y << self
75
+ descendants.each { |x| y << x }
76
+ end
77
+ end
78
+
79
+ # Returns depth of current node
80
+ def depth
81
+ ancestors.count
82
+ end
83
+ alias level depth
84
+
85
+ # Return +true+ if +self+ is root node
86
+ def root?
87
+ self[parent_column].nil?
88
+ end
89
+
90
+ # Return +true+ if +self+ is leaf node
91
+ def leaf?
92
+ children.empty?
93
+ end
94
+
95
+ # Returns true if record has changes in +parent_id+
96
+ def parent_changed?
97
+ changes.has_key?(parent_column.to_s)
98
+ end
99
+
100
+ # Move node to other parent, make it last child of new parent
101
+ def move_to_child_of(another_parent)
102
+ transaction do
103
+ self.parent = another_parent
104
+
105
+ p_changed = parent_changed?
106
+ save if p_changed
107
+
108
+ skip_reorder_callbacks(p_changed) { move_to_bottom }
109
+
110
+ parent.children.reload
111
+ end
112
+ end
113
+
114
+ # Move node to position of +another_node+, shift down lower items
115
+ def move_to_above_of(another_node)
116
+ p_changed = parent != another_node.parent
117
+
118
+ transaction do
119
+ move_to_child_of(another_node.parent)
120
+
121
+ skip_reorder_callbacks(p_changed) do
122
+ insert_at(another_node[position_column])
123
+ end
124
+
125
+ another_node.parent.children.reload if another_node.parent.present?
126
+ another_node.reload
127
+ end
128
+ end
129
+
130
+ # Move node to the next of +another_node+, shift down lower items
131
+ def move_to_bottom_of(another_node)
132
+ p_changed = parent != another_node.parent
133
+
134
+ transaction do
135
+ move_to_child_of(another_node.parent)
136
+
137
+ skip_reorder_callbacks(p_changed) do
138
+ insert_at(another_node[position_column] + 1)
139
+ end
140
+
141
+ another_node.parent.children.reload if another_node.parent.present?
142
+ another_node.reload
143
+ end
144
+ end
145
+
146
+ protected
147
+ def validate_incest #:nodoc:
148
+ errors.add(:parent, :linked_to_self) if parent == self
149
+ errors.add(:parent, :linked_to_descendant) if descendants.include?(parent)
150
+ end
151
+
152
+ def __around_move #:nodoc:
153
+ run_callbacks(:move) { yield }
154
+ end
155
+ end # module Tree
156
+ end # module ActsAsOrderedTree
@@ -0,0 +1,3 @@
1
+ module ActsAsOrderedTree
2
+ VERSION = "0.0.7"
3
+ end
@@ -0,0 +1,52 @@
1
+ require "enumerator"
2
+
3
+ require "active_record"
4
+ require "acts_as_list"
5
+ require "acts_as_tree"
6
+
7
+ require "acts_as_ordered_tree/version"
8
+ require "acts_as_ordered_tree/iterator"
9
+ require "acts_as_ordered_tree/tree"
10
+ require "acts_as_ordered_tree/list"
11
+
12
+ module ActsAsOrderedTree
13
+ def acts_as_ordered_tree(options = {})
14
+ configuration = configure_ordered_tree(options)
15
+
16
+ acts_as_tree :foreign_key => parent_column,
17
+ :order => position_column,
18
+ :counter_cache => configuration[:counter_cache]
19
+
20
+ acts_as_list :column => position_column,
21
+ :scope => parent_column
22
+
23
+ # acts_as_tree creates ugly associations
24
+ # patch them
25
+ children = reflect_on_association :children
26
+ children.options[:order] = quoted_position_column
27
+
28
+ include ActsAsOrderedTree::Tree
29
+ include ActsAsOrderedTree::List
30
+ end # def acts_as_ordered_tree
31
+
32
+ private
33
+ # Add ordered_tree configuration readers
34
+ def configure_ordered_tree(options = {}) #:nodoc:
35
+ configuration = { :foreign_key => :parent_id ,
36
+ :order => :position }
37
+ configuration.update(options) if options.is_a?(Hash)
38
+
39
+ class_attribute :parent_column, :position_column
40
+
41
+ self.parent_column = configuration[:foreign_key].to_sym
42
+ self.position_column = configuration[:order].to_sym
43
+
44
+ configuration
45
+ end # def configure_ordered_tree
46
+
47
+ def quoted_position_column #:nodoc:
48
+ [quoted_table_name, connection.quote_column_name(position_column)].join('.')
49
+ end # def quoted_position_column
50
+ end # module ActsAsOrderedTree
51
+
52
+ ActiveRecord::Base.extend(ActsAsOrderedTree)
@@ -0,0 +1,314 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe ActsAsOrderedTree do
4
+ before :all do
5
+ root = Node.create(:name => "Root")
6
+ child1 = Node.create(:parent_id => root.id, :name => "Child 1")
7
+ child2 = Node.create(:parent_id => root.id, :name => "Child 2")
8
+
9
+ Node.create(:parent_id => child1.id, :name => "Subchild 1")
10
+ Node.create(:parent_id => child1.id, :name => "Subchild 2")
11
+ Node.create(:parent_id => child2.id, :name => "Subchild 3")
12
+ end
13
+
14
+ let(:root) { Node.where(:parent_id => nil).first }
15
+ let(:branch) { Node.where(:parent_id => root.id).first }
16
+ let(:second_branch) { Node.where(:parent_id => root.id).last }
17
+ let(:leaf) { Node.where(:parent_id => branch.id).first }
18
+ let(:last) { Node.last }
19
+ let(:blank) { Node.new(:parent_id => branch.id) }
20
+
21
+ describe "class" do
22
+ it "should be properly configured" do
23
+ Node.position_column.should eq(:position)
24
+ Node.parent_column.should eq(:parent_id)
25
+ end
26
+
27
+ it "should have roots" do
28
+ Node.roots.count.should eq(1)
29
+ Node.roots.first.should eq(root)
30
+ Node.root.should eq(root)
31
+ end
32
+ end
33
+
34
+ describe "tree" do
35
+ it "should have roots" do
36
+ root.root.should eq(root)
37
+ branch.root.should eq(root)
38
+ leaf.root.should eq(root)
39
+ end
40
+
41
+ it "should have children" do
42
+ root.children.count.should eq(2)
43
+ branch.children.count.should eq(2)
44
+ leaf.children.count.should eq(0)
45
+ end
46
+
47
+ it "should have parents" do
48
+ root.parent.should be(nil)
49
+ branch.parent.should eq(root)
50
+ leaf.parent.should eq(branch)
51
+ end
52
+
53
+ it "should return true if root" do
54
+ root.root?.should be(true)
55
+ branch.root?.should be(false)
56
+ leaf.root?.should be(false)
57
+ end
58
+
59
+ it "should return true if leaf" do
60
+ root.leaf?.should be(false)
61
+ branch.leaf?.should be(false)
62
+ leaf.leaf?.should be(true)
63
+ end
64
+
65
+ it "should tell about node's depth" do
66
+ root.depth.should eq(0)
67
+ branch.depth.should eq(1)
68
+ leaf.depth.should eq(2)
69
+ end
70
+
71
+ it "should iterate over ancestors" do
72
+ leaf.self_and_ancestors.should have(3).items
73
+ leaf.ancestors.should have(2).items
74
+ branch.ancestors.should have(1).items
75
+ root.ancestors.should have(0).items
76
+ end
77
+
78
+ it "should iterate over descendants" do
79
+ root.self_and_descendants.should have(6).items
80
+
81
+ root.descendants.should have(5).items
82
+ root.descendants.first.should eq(branch)
83
+ root.descendants.last.should eq(last)
84
+
85
+ branch.descendants.should have(2).items
86
+ branch.descendants.first.should eq(leaf)
87
+
88
+ leaf.descendants.should have(0).items
89
+ end
90
+
91
+ it "should have siblings" do
92
+ branch.self_and_siblings.should have(2).items
93
+ branch.self_and_siblings.should include(branch)
94
+
95
+ branch.siblings.should have(1).item
96
+ branch.siblings.should_not include(branch)
97
+ end
98
+ end
99
+
100
+ describe "list" do
101
+ it "should be ordered" do
102
+ root.position.should eq(1)
103
+ root.children.first.position.should eq(1)
104
+ root.children.last.position.should eq(2)
105
+ end
106
+
107
+ it "should be sortable through scope" do
108
+ Node.where(:parent_id => root.id).ordered.first.should eq(branch)
109
+ end
110
+ end
111
+
112
+ describe "mutations" do
113
+ around(:each) do |example|
114
+ Node.transaction do
115
+ example.run
116
+
117
+ raise ActiveRecord::Rollback
118
+ end
119
+ end
120
+
121
+ it "should be placed to the bottom of the list" do
122
+ blank.save
123
+ branch.children.last.should eq(blank)
124
+ end
125
+
126
+ it "should be placed to the middle of the list" do
127
+ blank.position = 2
128
+ blank.save
129
+
130
+ blank.position.should eq(2)
131
+ blank.siblings.should have(2).items
132
+ blank.siblings.last.position.should eq(3)
133
+ end
134
+
135
+ it "should be movable inside parent" do
136
+ last_child = branch.children.last
137
+
138
+ blank.save
139
+ blank.move_higher
140
+
141
+ blank.position.should eq(2)
142
+ last_child.reload.position.should eq(3)
143
+
144
+ blank.move_lower
145
+ blank.position.should eq(3)
146
+ end
147
+
148
+ it "should be movable to bottom of its parent" do
149
+ first_child = branch.children.first
150
+
151
+ first_child.move_to_bottom
152
+ first_child.position.should eq(2)
153
+ first_child.reload.position.should eq(2)
154
+ end
155
+
156
+ it "should be movable to top of its parent" do
157
+ first_child = branch.children.first
158
+ last_child = branch.children.last
159
+
160
+ last_child.move_to_top
161
+
162
+ last_child.position.should eq(1)
163
+ last_child.reload.position.should eq(1)
164
+
165
+ first_child.reload.position.should eq(2)
166
+ end
167
+
168
+ it "should shift up lower items when parent is changed" do
169
+ first_child = branch.children.first
170
+ last_child = branch.children.last
171
+
172
+ # move to other parent
173
+ first_child.parent = second_branch
174
+ first_child.should be_parent_changed
175
+
176
+ first_child.save
177
+
178
+ # old sibling should shift up
179
+ last_child.reload.position.should eq(1)
180
+ end
181
+
182
+ it "should save its previous position when parent is changed" do
183
+ first_child = branch.children.first
184
+
185
+ first_child.parent = second_branch
186
+ first_child.save
187
+
188
+ first_child.position.should eq(1)
189
+ last.position.should eq(2)
190
+ end
191
+
192
+ it "should be movable to last position of new parent" do
193
+ first_child = branch.children.first
194
+
195
+ first_child.move_to_child_of(second_branch)
196
+ first_child.parent.should eq(second_branch)
197
+ first_child.should be_last
198
+ end
199
+
200
+ it "should be movable to above of some node" do
201
+ first_child = branch.children.first
202
+ above_of = second_branch.children.first
203
+
204
+ first_child.move_to_above_of(above_of)
205
+ first_child.parent.should eq(second_branch)
206
+
207
+ first_child.position.should eq(1)
208
+ above_of.position.should eq(2)
209
+ end
210
+
211
+ it "should be movable to bottom of some node" do
212
+ second = second_branch
213
+
214
+ first_child = branch.children.first
215
+
216
+ first_child.move_to_bottom_of(branch)
217
+ first_child.parent.should eq(branch.parent)
218
+
219
+ first_child.position.should eq(2)
220
+ second.reload.position.should eq(3)
221
+ end
222
+
223
+ it "should shift up lower items on destroy" do
224
+ branch.children.first.destroy
225
+
226
+ branch.children.should have(1).items
227
+ branch.children.first.position.should eq(1)
228
+ end
229
+
230
+ describe "callbacks" do
231
+ it "should fire *_reorder callbacks when position (but not parent) changes" do
232
+ examples_count = 6
233
+
234
+ second_branch.should_receive(:on_before_reorder).exactly(examples_count)
235
+ second_branch.should_receive(:on_around_reorder).exactly(examples_count)
236
+ second_branch.should_receive(:on_after_reorder).exactly(examples_count)
237
+
238
+ second_branch.move_higher
239
+ second_branch.move_lower
240
+ second_branch.move_to_top
241
+ second_branch.move_to_bottom
242
+ second_branch.decrement_position
243
+ second_branch.increment_position
244
+ end
245
+
246
+ it "should not fire *_reorder callbacks when parent_changes" do
247
+ leaf.should_not_receive(:on_before_reorder)
248
+ leaf.should_not_receive(:on_around_reorder)
249
+ leaf.should_not_receive(:on_after_reorder)
250
+
251
+ p1 = leaf.parent
252
+ p2 = second_branch
253
+
254
+ leaf.move_to_child_of(p2)
255
+ leaf.move_to_above_of(p1.children.first)
256
+ leaf.move_to_child_of(p2)
257
+ leaf.move_to_bottom_of(p1.children.first)
258
+ end
259
+
260
+ it "should not fire *_reorder callbacks when position is not changed" do
261
+ leaf.should_not_receive(:on_before_reorder)
262
+ leaf.should_not_receive(:on_around_reorder)
263
+ leaf.should_not_receive(:on_after_reorder)
264
+
265
+ last.should_not_receive(:on_before_reorder)
266
+ last.should_not_receive(:on_around_reorder)
267
+ last.should_not_receive(:on_after_reorder)
268
+
269
+ leaf.move_higher
270
+ last.move_lower
271
+
272
+ leaf.save
273
+ last.save
274
+ end
275
+
276
+ it "should fire *_move callbacks when parent is changed" do
277
+ examples_count = 3
278
+ leaf.should_receive(:on_before_move).exactly(examples_count)
279
+ leaf.should_receive(:on_after_move).exactly(examples_count)
280
+ leaf.should_receive(:on_around_move).exactly(examples_count)
281
+
282
+ p1 = leaf.parent
283
+ p2 = second_branch
284
+
285
+ leaf.move_to_child_of(p2)
286
+ leaf.move_to_above_of(p1)
287
+ leaf.move_to_bottom_of(p1.children.first)
288
+ end
289
+
290
+ it "should not fire *_move callbacks when parent is not changed" do
291
+ leaf.should_not_receive(:on_before_move)
292
+ leaf.should_not_receive(:on_after_move)
293
+ leaf.should_not_receive(:on_around_move)
294
+
295
+ leaf.move_to_child_of(leaf.parent)
296
+ leaf.move_to_above_of(leaf.siblings.first)
297
+ leaf.move_to_bottom_of(leaf.siblings.first)
298
+ leaf.reload.save
299
+ end
300
+ end
301
+ end
302
+
303
+ describe "validations" do
304
+ it "should not allow to link parent to itself" do
305
+ branch.parent = branch
306
+ branch.should_not be_valid
307
+ end
308
+
309
+ it "should not allow to link to one of its descendants" do
310
+ branch.parent = leaf
311
+ branch.should_not be_valid
312
+ end
313
+ end
314
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ database:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,73 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe ActsAsOrderedTree::Iterator do
4
+ let(:iterator) do
5
+ ActsAsOrderedTree::Iterator.new([1, 2, 3, 4, 2, 3])
6
+ end
7
+
8
+ let(:blanks) { ActsAsOrderedTree::Iterator.new([1, nil, 3]) }
9
+
10
+ it "should have random access" do
11
+ iterator[1].should eq(2)
12
+ iterator.at(1).should eq(2)
13
+ iterator.fetch(1).should eq(2)
14
+ iterator.values_at(1, 2).should eq([2, 3])
15
+ iterator.last.should eq(3)
16
+ iterator.slice(1, 2).should have(2).items
17
+ iterator.sample.should be_a(Fixnum)
18
+ end
19
+
20
+ it "should support operators" do
21
+ (iterator + [5]).should have(7).items
22
+ (iterator - [4]).should have(5).items
23
+ (iterator * 2).should have(12).items
24
+ (iterator & [4]).should have(1).items
25
+ (iterator | [4]).should have(4).items
26
+ iterator.concat([5]).should have(7).items
27
+ end
28
+
29
+ it "should find left index" do
30
+ iterator.find_index(2).should eq(1)
31
+ iterator.find_index { |n| n == 2 }.should eq(1)
32
+ end
33
+
34
+ it "should find right index" do
35
+ iterator.rindex(2).should eq(4)
36
+ iterator.rindex { |n| n == 2 }.should eq(4)
37
+ end
38
+
39
+ it "should be compacted" do
40
+ blanks.compact.should have(2).items
41
+ end
42
+
43
+ it "should be mutable" do
44
+ iter = ActsAsOrderedTree::Iterator.new([1, 2])
45
+ iter << 3 # [1, 2, 3]
46
+
47
+ iter.should have(3).items
48
+
49
+ iter.insert(1, 99) # [1, 99, 2, 3]
50
+ iter.at(1).should eq(99)
51
+
52
+ last = iter.pop # [1, 99, 2]
53
+ iter.last.should eq(2)
54
+ last.should eq(3)
55
+
56
+ first = iter.shift # [99, 2]
57
+ iter.first.should eq(99)
58
+ first.should eq(1)
59
+
60
+ iter.unshift(100) # [100, 99, 2]
61
+ iter.first.should eq(100)
62
+
63
+ iter.push(4)
64
+ iter.should have(4).items
65
+ iter.last.should eq(4)
66
+ end
67
+
68
+ it "should raise NoMethodError" do
69
+ iter = ActsAsOrderedTree::Iterator.new([1, 2])
70
+
71
+ lambda { iter.__undefined_method__ }.should raise_error(NoMethodError)
72
+ end
73
+ end
@@ -0,0 +1,53 @@
1
+ require File.expand_path('../../init', __FILE__)
2
+
3
+ require "rspec"
4
+ require "rspec-expectations"
5
+
6
+ require "simplecov"
7
+ SimpleCov.start
8
+
9
+ require "acts_as_ordered_tree"
10
+ require "logger"
11
+
12
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
13
+ ActiveRecord::Base.establish_connection(config['database'])
14
+ ActiveRecord::Base.logger = Logger.new(ENV['DEBUG'] ? $stderr : '/dev/null')
15
+
16
+ # Create schema
17
+ ActiveRecord::Base.connection.create_table :nodes do |t|
18
+ t.integer :parent_id
19
+ t.integer :position
20
+ t.string :name
21
+ end
22
+
23
+ class Node < ActiveRecord::Base
24
+ acts_as_ordered_tree
25
+
26
+ before_reorder :on_before_reorder
27
+ after_reorder :on_after_reorder
28
+ around_reorder :on_around_reorder
29
+ before_move :on_before_move
30
+ after_move :on_after_move
31
+ around_move :on_around_move
32
+
33
+ def self.debug
34
+ buf = StringIO.new("", "w")
35
+
36
+ roots.each do |n|
37
+ buf.puts "! #{n.name}"
38
+ n.descendants.each do |d|
39
+ buf.puts "#{' ' * d.level * 2} (##{d.id}): #{d.name} @ #{d.position}"
40
+ end
41
+ end
42
+
43
+ print buf.string
44
+ end
45
+
46
+ # stub
47
+ def on_before_reorder;end
48
+ def on_after_reorder;end
49
+ def on_around_reorder;yield end
50
+ def on_before_move; end
51
+ def on_after_move; end
52
+ def on_around_move; yield end
53
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_ordered_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.7
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alexei Mikhailov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-15 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &19980600 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *19980600
25
+ - !ruby/object:Gem::Dependency
26
+ name: activerecord
27
+ requirement: &19980040 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *19980040
36
+ - !ruby/object:Gem::Dependency
37
+ name: acts_as_tree
38
+ requirement: &19979360 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '0.1'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *19979360
47
+ - !ruby/object:Gem::Dependency
48
+ name: acts_as_list
49
+ requirement: &19978620 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '0.1'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *19978620
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &19977860 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *19977860
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: &19977100 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *19977100
80
+ - !ruby/object:Gem::Dependency
81
+ name: sqlite3
82
+ requirement: &19976120 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *19976120
91
+ - !ruby/object:Gem::Dependency
92
+ name: bundler
93
+ requirement: &19945820 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *19945820
102
+ description:
103
+ email:
104
+ - amikhailov83@gmail.com
105
+ executables: []
106
+ extensions: []
107
+ extra_rdoc_files: []
108
+ files:
109
+ - .gitignore
110
+ - Gemfile
111
+ - README.md
112
+ - Rakefile
113
+ - acts_as_ordered_tree.gemspec
114
+ - init.rb
115
+ - lib/acts_as_ordered_tree.rb
116
+ - lib/acts_as_ordered_tree/iterator.rb
117
+ - lib/acts_as_ordered_tree/list.rb
118
+ - lib/acts_as_ordered_tree/tree.rb
119
+ - lib/acts_as_ordered_tree/version.rb
120
+ - spec/acts_as_ordered_tree_spec.rb
121
+ - spec/database.yml
122
+ - spec/iterator_spec.rb
123
+ - spec/test_helper.rb
124
+ homepage: https://github.com/take-five/acts_as_ordered_tree
125
+ licenses: []
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project: acts_as_ordered_tree
144
+ rubygems_version: 1.8.10
145
+ signing_key:
146
+ specification_version: 3
147
+ summary: ActiveRecord extension for sorted adjacency lists support
148
+ test_files: []
149
+ has_rdoc: