diff --git a/lib/parentry.rb b/lib/parentry.rb index 5036b9a..1bcffcb 100644 --- a/lib/parentry.rb +++ b/lib/parentry.rb @@ -7,7 +7,7 @@ module Parentry def self.included(base) base.class_eval do - mattr_accessor :parentry_column, :depth_offset, :cache_depth + mattr_accessor :parentry_column, :depth_offset, :cache_depth, :touch_ancestors belongs_to :parent, class_name: base_class.name has_many :children, class_name: base_class.name, foreign_key: :parent_id, dependent: :destroy @@ -25,6 +25,10 @@ def self.included(base) after_update :cascade_parentry, if: proc { changes[parentry_column].present? } after_save :cache_parentry_depth, if: proc { cache_depth && depth != parentry_depth } + after_save :touch_ancestors_callback + after_touch :touch_ancestors_callback + after_destroy :touch_ancestors_callback + scope :order_by_parentry, -> { order("nlevel(#{parentry_column})") } scope :before_depth, ->(depth) { where("nlevel(#{parentry_column}) - 1 < ?", depth + depth_offset) } diff --git a/lib/parentry/class_methods.rb b/lib/parentry/class_methods.rb index 61967d7..041aa0b 100644 --- a/lib/parentry/class_methods.rb +++ b/lib/parentry/class_methods.rb @@ -4,6 +4,7 @@ def parentry(options = {}) self.parentry_column = options.fetch(:parentry_column, 'parentry') self.depth_offset = options.fetch(:depth_offset, 0) self.cache_depth = options.fetch(:cache_depth, false) + self.touch_ancestors = options.fetch(:touch, false) end def arrange(options = {}) diff --git a/lib/parentry/instance_methods.rb b/lib/parentry/instance_methods.rb index e6ff74a..12198bd 100644 --- a/lib/parentry/instance_methods.rb +++ b/lib/parentry/instance_methods.rb @@ -5,8 +5,8 @@ def parentry_scope end def prevent_circular_parentry - computed = compute_parentry - errors.add(:parentry, 'contains a circular reference') unless computed.split('.').uniq == computed.split('.') + computed = parse_parentry(compute_parentry) + errors.add(:parentry, 'contains a circular reference') unless computed.uniq == computed end def commit_parentry @@ -40,5 +40,33 @@ def compute_parentry def parentry read_attribute(parentry_column) end + + def parse_parentry(input = parentry) + input.to_s.split('.').map(&:to_i) + end + + def touch_ancestors_callback + return unless touch_ancestors + return if touch_callbacks_disabled? + + parentry_scope.where(id: ancestor_ids_was + ancestor_ids).each do |ancestor| + ancestor.without_touch_callbacks { ancestor.touch } + end + end + + def without_touch_callbacks + @disable_touch_callbacks = true + yield + @disable_touch_callbacks = false + end + + def touch_callbacks_disabled? + @disable_touch_callbacks + end + + def ancestor_ids_was + return [] unless changes[parentry_column] + parse_parentry(changes[parentry_column][0]).tap(&:pop) + end end end diff --git a/lib/parentry/navigation.rb b/lib/parentry/navigation.rb index feae817..560e705 100644 --- a/lib/parentry/navigation.rb +++ b/lib/parentry/navigation.rb @@ -13,7 +13,7 @@ def root? end def path_ids - parentry.split('.').map(&:to_i) + parse_parentry end def path(scopes = {}) diff --git a/spec/db/schema.rb b/spec/db/schema.rb index 1defc6b..ac59d77 100644 --- a/spec/db/schema.rb +++ b/spec/db/schema.rb @@ -6,10 +6,21 @@ t.ltree :parentry t.integer :parentry_depth t.integer :rank + + t.timestamps null: false end create_table :one_depth_tree_nodes, force: true do |t| t.integer :parent_id t.ltree :parentry + + t.timestamps null: false + end + + create_table :touch_tree_nodes, force: true do |t| + t.integer :parent_id + t.ltree :parentry + + t.timestamps null: false end end diff --git a/spec/instance_methods_spec.rb b/spec/instance_methods_spec.rb index 8624275..00a2432 100644 --- a/spec/instance_methods_spec.rb +++ b/spec/instance_methods_spec.rb @@ -64,4 +64,55 @@ expect(child.reload.parentry).to eq "#{node.parentry}.#{child.id}" end end + + context 'touch ancestors option' do + context 'enabled' do + it 'should update ancestor timestamp' do + parent = TouchTreeNode.create + expect do + parent.children.create + end.to change { parent.reload.updated_at } + end + + context 'parent changes' do + it 'should update old parent timestamp' do + parent = TouchTreeNode.create + node = parent.children.create + new_parent = TouchTreeNode.create + + expect do + node.update_attributes(parent: new_parent) + end.to change { parent.reload.updated_at } + end + + it 'should update new parent timestamp' do + parent = TouchTreeNode.create + node = parent.children.create + new_parent = TouchTreeNode.create + + expect do + node.update_attributes(parent: new_parent) + end.to change { new_parent.reload.updated_at } + end + end + + it 'should update parent timestamp when child is deleted' do + parent = TouchTreeNode.create + node = parent.children.create + + expect do + node.destroy + end.to change { parent.reload.updated_at } + end + end + + context 'disabled' do + it 'should not update ancestor timestamp' do + parent = TreeNode.create + expect do + parent.children.create + end.not_to change { parent.reload.updated_at } + end + end + end end diff --git a/spec/support/models.rb b/spec/support/models.rb index 5a7d965..a63bc43 100644 --- a/spec/support/models.rb +++ b/spec/support/models.rb @@ -7,3 +7,8 @@ class OneDepthTreeNode < ActiveRecord::Base include Parentry parentry depth_offset: 1 end + +class TouchTreeNode < ActiveRecord::Base + include Parentry + parentry touch: true +end