-
Notifications
You must be signed in to change notification settings - Fork 459
/
class_methods.rb
187 lines (170 loc) · 7.34 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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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, adopt, restrict or destroy is allowed
if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
class_variable_set :@@orphan_strategy, orphan_strategy
else
raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify,:adopt, :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
arrange_nodes scope.where(options)
end
# Arrange array of nodes into a nested hash of the form
# {node => children}, where children = {} if the node has no children
def arrange_nodes(nodes)
# Get all nodes ordered by ancestry and start sorting them into an empty hash
nodes.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
# Pseudo-preordered array of nodes. Children will always follow parents,
# for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
def sort_by_ancestry(nodes, &block)
arranged = nodes if nodes.is_a?(Hash)
unless arranged
presorted_nodes = nodes.sort do |a, b|
a_cestry, b_cestry = a.ancestry || '0', b.ancestry || '0'
if block_given? && a_cestry == b_cestry
yield a, b
else
a_cestry <=> b_cestry
end
end
arranged = arrange_nodes(presorted_nodes)
end
arranged.inject([]) do |sorted_nodes, pair|
node, children = pair
sorted_nodes << node
sorted_nodes += sort_by_ancestry(children, &block) unless children.blank?
sorted_nodes
end
end
# Integrity checking
def check_ancestry_integrity! options = {}
parents = {}
exceptions = [] if options[:report] == :list
self.base_class.unscoped do
# For each node ...
self.base_class.find_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
end
exceptions if options[:report] == :list
end
# Integrity restoration
def restore_ancestry_integrity!
parents = {}
# Wrap the whole thing in a transaction ...
self.base_class.transaction do
self.base_class.unscoped do
# For each node ...
self.base_class.find_each do |node|
# ... set its ancestry to nil if invalid
if !node.valid? and !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.find_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
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.unscoped do
self.base_class.where(:parent_id => parent_id).find_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
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.unscoped do
self.base_class.find_each do |node|
node.update_attribute depth_cache_column, node.depth
end
end
end
end
end