Skip to content

Commit

Permalink
Support for before/after add/remove callbacks
Browse files Browse the repository at this point in the history
It is available now to setup custom callbacks which will be
fired when node created, moved or destroyed via options
:before_add, :after_add, :before_remove, :after_remove.

Closes #25
  • Loading branch information
take-five committed Feb 18, 2014
1 parent cc11514 commit 4665b07
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 16 deletions.
10 changes: 8 additions & 2 deletions lib/acts_as_ordered_tree/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ def initialize(record)

# Returns scope to which record should be applied
def scope
base_class = if record.class.finder_needs_type_condition?
record.class.base_class
else
record.class
end

if tree.columns.scope?
record.class.base_class.where Hash[tree.columns.scope.map { |column| [column, record[column]] }]
base_class.where Hash[tree.columns.scope.map { |column| [column, record[column]] }]
else
record.class.base_class.where(nil)
base_class.where(nil)
end
end

Expand Down
5 changes: 5 additions & 0 deletions lib/acts_as_ordered_tree/transaction/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def klass
def transaction
@transaction ||= PerseveringTransaction.new(connection)
end

# Trigger tree callback (before_add, after_add, before_remove, after_remove)
def trigger_callback(kind, owner)
tree.callbacks.send(kind, owner, record) if owner.present?
end
end
end
end
3 changes: 3 additions & 0 deletions lib/acts_as_ordered_tree/transaction/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ class Create < Save
before :push_to_bottom_after_commit, :if => 'push_to_bottom? && to.root?'
before :set_counter_cache, :if => 'tree.columns.counter_cache?'
before :increment_lower_positions, :unless => :push_to_bottom?
before 'trigger_callback(:before_add, to.parent)'

after 'to.increment_counter'
after 'trigger_callback(:after_add, to.parent)'

private
def set_counter_cache
Expand Down
3 changes: 3 additions & 0 deletions lib/acts_as_ordered_tree/transaction/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ class Destroy < Base

attr_reader :from

before 'trigger_callback(:before_remove, from.parent)'

after :decrement_lower_positions
after 'from.decrement_counter'
after 'trigger_callback(:after_remove, from.parent)'

# @param [ActsAsOrderedTree::Node] node
# @param [ActsAsOrderedTree::Position] from from which position given +node+ is destroyed
Expand Down
5 changes: 5 additions & 0 deletions lib/acts_as_ordered_tree/transaction/move.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class Move < Update
movement.before :update_tree
movement.before 'transition.update_counters'
end
before 'trigger_callback(:before_remove, from.parent)'
before 'trigger_callback(:before_add, to.parent)'

before :update_descendants_depth, :if => [
'transition.movement?',
Expand All @@ -18,6 +20,9 @@ class Move < Update
'record.children.size > 0'
]

after 'trigger_callback(:after_add, to.parent)'
after 'trigger_callback(:after_remove, from.parent)'

private
def callbacks(&block)
record.run_callbacks(:move, &block)
Expand Down
6 changes: 5 additions & 1 deletion lib/acts_as_ordered_tree/tree/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ def initialize(tree)

protected
def class_name
"::#{klass.base_class.name}"
if klass.finder_needs_type_condition?
"::#{klass.base_class.name}"
else
"::#{klass.name}"
end
end
end # class Association
end # class Tree
Expand Down
43 changes: 30 additions & 13 deletions lib/acts_as_ordered_tree/tree/callbacks.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# coding: utf-8

require 'active_support/core_ext/hash/slice'

module ActsAsOrderedTree
class Tree
# Tree callbacks storage
#
# @example
# MyModel.ordered_tree.callbacks.before_add? # => false
# MyModel.ordered_tree
# MyModel.ordered_tree.callbacks.before_add(parent, child)
#
# @api private
class Callbacks
VALID_KEYS = :before_add,
:after_add,
:before_remove,
:after_remove
:after_add,
:before_remove,
:after_remove

def initialize(klass, options)
@klass = klass
Expand All @@ -27,18 +30,32 @@ def to_h
end

# generate accessors and predicates
VALID_KEYS.each do |k|
define_method(k) do |owner, record| # def before_add(parent, record)
if @callbacks.key?(k)
raise NotImplementedError, "#{k} callbacks not implemented yet"
end
VALID_KEYS.each do |method|
define_method(method) do |parent, record| # def before_add(parent, record)
run_callbacks(method, parent, record)
end
end

# def before_add?() @callbacks.key?(:before_add) end
define_method("#{k}?") do
@callbacks.key?(k)
private
def run_callbacks(method, parent, record)
callback = callback_for(method)

case callback
when Symbol
parent.send(callback, record)
when Proc
callback.call(parent, record)
when nil, false
# do nothing
else
# parent.before_add(record)
callback.send(method, parent, record)
end
end

def callback_for(method)
@callbacks[method]
end
end # class Callbacks
end # class Tree
end # module ActsAsOrderedTree
93 changes: 93 additions & 0 deletions spec/move_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,97 @@ def move(node, new_parent, new_position)

it { expect(root.position).to eq 2 }
end
end

describe ActsAsOrderedTree, 'before/after add/remove callbacks', :transactional do
class Class1 < Default
cattr_accessor :triggered_callbacks

def self.triggered?(kind, *args)
triggered_callbacks.include?([kind, *args])
end

acts_as_ordered_tree :before_add => :before_add,
:after_add => :after_add,
:before_remove => :before_remove,
:after_remove => :after_remove

def run_callback(kind, arg)
self.class.triggered_callbacks ||= []
self.class.triggered_callbacks << [kind, self, arg]
end

def before_remove(record)
run_callback(:before_remove, record)
end

def after_remove(record)
run_callback(:after_remove, record)
end

def before_add(record)
run_callback(:before_add, record)
end

def after_add(record)
run_callback(:after_add, record)
end
end

# root
# child 1
# child 2
# child 3
# child 4
# child 5
let(:root) { Class1.create :name => 'root' }
let!(:child1) { Class1.create :name => 'child1', :parent => root }
let!(:child2) { Class1.create :name => 'child2', :parent => child1 }
let!(:child3) { Class1.create :name => 'child3', :parent => child1 }
let!(:child4) { Class1.create :name => 'child4', :parent => root }
let!(:child5) { Class1.create :name => 'child5', :parent => child4 }

def test_callback(record, kind, expected_arg, &block)
Class1.triggered_callbacks = []

expect(&block).to change{Class1.triggered?(kind, record, expected_arg)}.to(true)
end

describe '*_add callbacks' do
let(:new_record) { Class1.new :name => 'child6' }

it 'fires before_add callback when new children added to node' do
test_callback(child1, :before_add, new_record) { child1.children << new_record }
end

it 'fires after_add callback when new children added to node' do
test_callback(child1, :after_add, new_record) { child1.children << new_record }
end

it 'fires before_add callback when node is moved from another parent' do
test_callback(child4, :before_add, child2) { child2.update_attributes!(:parent => child4) }
end

it 'fires after_add callback when node is moved from another parent' do
test_callback(child4, :after_add, child2) { child2.update_attributes!(:parent => child4) }
end
end

describe '*_remove callbacks' do
it 'fires before_remove callback when node is moved from another parent' do
test_callback(child1, :before_remove, child2) { child2.update_attributes!(:parent => child4) }
end

it 'fires after_remove callback when node is moved from another parent' do
test_callback(child1, :after_remove, child2) { child2.update_attributes!(:parent => child4) }
end

it 'fires before_remove callback when node is destroyed' do
test_callback(child1, :before_remove, child2) { child2.destroy }
end

it 'fires after_remove callback when node is destroyed' do
test_callback(child1, :after_remove, child2) { child2.destroy }
end
end
end

0 comments on commit 4665b07

Please sign in to comment.