acts_as_ordered_tree 0.0.7 → 1.0.3
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 +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
|