-
Notifications
You must be signed in to change notification settings - Fork 459
/
instance_methods.rb
246 lines (204 loc) · 7.9 KB
/
instance_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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
module Ancestry
module InstanceMethods
# Validate that the ancestors don't include itself
def ancestry_exclude_self
errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") if ancestor_ids.include? self.id
end
# Update descendants with new ancestry
def update_descendants_with_new_ancestry
# Skip this if callbacks are disabled
unless ancestry_callbacks_disabled?
# If node is not a new record and ancestry was updated and the new ancestry is sane ...
if changed.include?(self.base_class.ancestry_column.to_s) && !new_record? && sane_ancestry?
# ... for each descendant ...
unscoped_descendants.each do |descendant|
# ... replace old ancestry with new ancestry
descendant.without_ancestry_callbacks do
descendant.update_attribute(
self.base_class.ancestry_column,
descendant.read_attribute(descendant.class.ancestry_column).gsub(
/^#{self.child_ancestry}/,
if read_attribute(self.class.ancestry_column).blank? then id.to_s else "#{read_attribute self.class.ancestry_column }/#{id}" end
)
)
end
end
end
end
end
# Apply orphan strategy
def apply_orphan_strategy
# Skip this if callbacks are disabled
unless ancestry_callbacks_disabled?
# If this isn't a new record ...
unless new_record?
# ... make all children root if orphan strategy is rootify
if self.base_class.orphan_strategy == :rootify
unscoped_descendants.each do |descendant|
descendant.without_ancestry_callbacks do
descendant.update_attribute descendant.class.ancestry_column, (if descendant.ancestry == child_ancestry then nil else descendant.ancestry.gsub(/^#{child_ancestry}\//, '') end)
end
end
# ... destroy all descendants if orphan strategy is destroy
elsif self.base_class.orphan_strategy == :destroy
unscoped_descendants.each do |descendant|
descendant.without_ancestry_callbacks do
descendant.destroy
end
end
# ... make child elements of this node, child of its parent if orphan strategy is adopt
elsif self.base_class.orphan_strategy == :adopt
descendants.all.each do |descendant|
descendant.without_ancestry_callbacks do
new_ancestry = descendant.ancestor_ids.delete_if { |x| x == self.id }.join("/")
descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
end
end
# ... throw an exception if it has children and orphan strategy is restrict
elsif self.base_class.orphan_strategy == :restrict
raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
end
end
end
end
# The ancestry value for this record's children
def child_ancestry
# New records cannot have children
raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
if self.send("#{self.base_class.ancestry_column}_was").blank? then id.to_s else "#{self.send "#{self.base_class.ancestry_column}_was"}/#{id}" end
end
# Ancestors
def ancestor_ids
read_attribute(self.base_class.ancestry_column).to_s.split('/').map { |id| cast_primary_key(id) }
end
def ancestor_conditions
{self.base_class.primary_key => ancestor_ids}
end
def ancestors depth_options = {}
self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
end
def path_ids
ancestor_ids + [id]
end
def path_conditions
{self.base_class.primary_key => path_ids}
end
def path depth_options = {}
self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
end
def depth
ancestor_ids.size
end
def cache_depth
write_attribute self.base_class.depth_cache_column, depth
end
# Parent
def parent= parent
write_attribute(self.base_class.ancestry_column, if parent.blank? then nil else parent.child_ancestry end)
end
def parent_id= parent_id
self.parent = if parent_id.blank? then nil else unscoped_find(parent_id) end
end
def parent_id
if ancestor_ids.empty? then nil else ancestor_ids.last end
end
def parent
if parent_id.blank? then nil else unscoped_find(parent_id) end
end
# Root
def root_id
if ancestor_ids.empty? then id else ancestor_ids.first end
end
def root
if root_id == id then self else unscoped_find(root_id) end
end
def is_root?
read_attribute(self.base_class.ancestry_column).blank?
end
# Children
def child_conditions
{self.base_class.ancestry_column => child_ancestry}
end
def children
self.base_class.where child_conditions
end
def child_ids
children.select(self.base_class.primary_key).map(&self.base_class.primary_key.to_sym)
end
def has_children?
self.children.exists?({})
end
def is_childless?
!has_children?
end
# Siblings
def sibling_conditions
{self.base_class.ancestry_column => read_attribute(self.base_class.ancestry_column)}
end
def siblings
self.base_class.where sibling_conditions
end
def sibling_ids
siblings.select(self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
end
def has_siblings?
self.siblings.count > 1
end
def is_only_child?
!has_siblings?
end
# Descendants
def descendant_conditions
["#{self.base_class.table_name}.#{self.base_class.ancestry_column} like ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
end
def descendants depth_options = {}
self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
end
def descendant_ids depth_options = {}
descendants(depth_options).select(self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
end
# Subtree
def subtree_conditions
["#{self.base_class.table_name}.#{self.base_class.primary_key} = ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} like ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
end
def subtree depth_options = {}
self.base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
end
def subtree_ids depth_options = {}
subtree(depth_options).select(self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
end
# Callback disabling
def without_ancestry_callbacks
@disable_ancestry_callbacks = true
yield
@disable_ancestry_callbacks = false
end
def ancestry_callbacks_disabled?
!!@disable_ancestry_callbacks
end
private
def cast_primary_key(key)
if primary_key_type == :string
key
else
key.to_i
end
end
def primary_key_type
@primary_key_type ||= column_for_attribute(self.class.primary_key).type
end
def unscoped_descendants
self.base_class.unscoped do
self.base_class.where descendant_conditions
end
end
# basically validates the ancestry, but also applied if validation is
# bypassed to determine if chidren should be affected
def sane_ancestry?
ancestry.nil? || (ancestry.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
end
def unscoped_find id
self.base_class.unscoped { self.base_class.find(id) }
end
end
end