acts_as_ordered_tree 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: