forked from stefankroes/ancestry
-
Notifications
You must be signed in to change notification settings - Fork 0
/
class_methods.rb
137 lines (129 loc) · 5.65 KB
/
class_methods.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
module Ancestry
module ClassMethods
# Fetch tree node if necessary
def to_node object
if object.is_a?(self.base_class) then object else find(object) end
end
# Scope on relative depth options
def scope_depth depth_options, depth
depth_options.inject(self.base_class) do |scope, option|
scope_name, relative_depth = option
if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
scope.send scope_name, depth + relative_depth
else
raise Ancestry::AncestryException.new("Unknown depth option: #{scope_name}.")
end
end
end
# Orphan strategy writer
def orphan_strategy= orphan_strategy
# Check value of orphan strategy, only rootify, restrict or destroy is allowed
if [:rootify, :restrict, :destroy].include? orphan_strategy
class_variable_set :@@orphan_strategy, orphan_strategy
else
raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
end
end
# Arrangement
def arrange options = {}
scope =
if options[:order].nil?
self.base_class.ordered_by_ancestry
else
self.base_class.ordered_by_ancestry_and options.delete(:order)
end
# Get all nodes ordered by ancestry and start sorting them into an empty hash
scope.all(options).inject(ActiveSupport::OrderedHash.new) do |arranged_nodes, node|
# Find the insertion point for that node by going through its ancestors
node.ancestor_ids.inject(arranged_nodes) do |insertion_point, ancestor_id|
insertion_point.each do |parent, children|
# Change the insertion point to children if node is a descendant of this parent
insertion_point = children if ancestor_id == parent.id
end; insertion_point
end[node] = ActiveSupport::OrderedHash.new; arranged_nodes
end
end
# Integrity checking
def check_ancestry_integrity! options = {}
parents = {}
exceptions = [] if options[:report] == :list
# For each node ...
self.base_class.all.each do |node|
begin
# ... check validity of ancestry column
if !node.valid? and !node.errors[node.class.ancestry_column].blank?
raise Ancestry::AncestryIntegrityException.new "Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}."
end
# ... check that all ancestors exist
node.ancestor_ids.each do |ancestor_id|
unless exists? ancestor_id
raise Ancestry::AncestryIntegrityException.new "Reference to non-existent node in node #{node.id}: #{ancestor_id}."
end
end
# ... check that all node parents are consistent with values observed earlier
node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
parents[node_id] = parent_id unless parents.has_key? node_id
unless parents[node_id] == parent_id
raise Ancestry::AncestryIntegrityException.new "Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}"
end
end
rescue Ancestry::AncestryIntegrityException => integrity_exception
case options[:report]
when :list then exceptions << integrity_exception
when :echo then puts integrity_exception
else raise integrity_exception
end
end
end
exceptions if options[:report] == :list
end
# Integrity restoration
def restore_ancestry_integrity!
parents = {}
# For each node ...
self.base_class.all.each do |node|
# ... set its ancestry to nil if invalid
if node.errors[node.class.ancestry_column].blank?
node.without_ancestry_callbacks do
node.update_attribute node.ancestry_column, nil
end
end
# ... save parent of this node in parents array if it exists
parents[node.id] = node.parent_id if exists? node.parent_id
# Reset parent id in array to nil if it introduces a cycle
parent = parents[node.id]
until parent.nil? || parent == node.id
parent = parents[parent]
end
parents[node.id] = nil if parent == node.id
end
# For each node ...
self.base_class.all.each do |node|
# ... rebuild ancestry from parents array
ancestry, parent = nil, parents[node.id]
until parent.nil?
ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
end
node.without_ancestry_callbacks do
node.update_attribute node.ancestry_column, ancestry
end
end
end
# Build ancestry from parent id's for migration purposes
def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
self.base_class.all(:conditions => {:parent_id => parent_id}).each do |node|
node.without_ancestry_callbacks do
node.update_attribute ancestry_column, ancestry
end
build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
end
end
# Rebuild depth cache if it got corrupted or if depth caching was just turned on
def rebuild_depth_cache!
raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
self.base_class.all.each do |node|
node.update_attribute depth_cache_column, node.depth
end
end
end
end