acts_as_ordered_tree 0.0.7 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Appraisals +11 -0
- data/Gemfile +5 -1
- data/README.md +44 -10
- data/Rakefile +4 -0
- data/acts_as_ordered_tree.gemspec +12 -11
- data/lib/acts_as_ordered_tree.rb +99 -34
- data/lib/acts_as_ordered_tree/class_methods.rb +29 -0
- data/lib/acts_as_ordered_tree/fake_scope.rb +28 -0
- data/lib/acts_as_ordered_tree/instance_methods.rb +375 -0
- data/lib/acts_as_ordered_tree/validators.rb +15 -0
- data/lib/acts_as_ordered_tree/version.rb +1 -1
- data/spec/acts_as_ordered_tree_spec.rb +710 -205
- data/spec/db/schema.rb +23 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/factories.rb +14 -0
- data/spec/support/matchers.rb +132 -0
- data/spec/support/models.rb +23 -0
- metadata +118 -51
- data/init.rb +0 -2
- data/lib/acts_as_ordered_tree/iterator.rb +0 -36
- data/lib/acts_as_ordered_tree/list.rb +0 -100
- data/lib/acts_as_ordered_tree/tree.rb +0 -156
- data/spec/database.yml +0 -3
- data/spec/iterator_spec.rb +0 -73
- data/spec/test_helper.rb +0 -53
data/init.rb
DELETED
@@ -1,36 +0,0 @@
|
|
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
|
@@ -1,100 +0,0 @@
|
|
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
|
@@ -1,156 +0,0 @@
|
|
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
|
data/spec/database.yml
DELETED
data/spec/iterator_spec.rb
DELETED
@@ -1,73 +0,0 @@
|
|
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
|
data/spec/test_helper.rb
DELETED
@@ -1,53 +0,0 @@
|
|
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
|