Module: Ancestry::InstanceMethods

Defined in:
lib/ancestry/instance_methods.rb

Instance Method Summary collapse

Instance Method Details

#ancestor_of?(node) ⇒ Boolean

Returns:

  • (Boolean)


152
153
154
# File 'lib/ancestry/instance_methods.rb', line 152

def ancestor_of?(node)
  node.ancestor_ids.include?(self.id)
end

#ancestors(depth_options = {}) ⇒ Object



123
124
125
126
# File 'lib/ancestry/instance_methods.rb', line 123

def ancestors depth_options = {}
  return self.ancestry_base_class.none unless has_parent?
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
end

#ancestry_callbacks_disabled?Boolean

Returns:

  • (Boolean)


304
305
306
# File 'lib/ancestry/instance_methods.rb', line 304

def ancestry_callbacks_disabled?
  defined?(@disable_ancestry_callbacks) && @disable_ancestry_callbacks
end

#ancestry_changed?Boolean

Returns:

  • (Boolean)


99
100
101
102
103
104
# File 'lib/ancestry/instance_methods.rb', line 99

def ancestry_changed?
  column = self.ancestry_base_class.ancestry_column.to_s
    # These methods return nil if there are no changes.
    # This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
    !!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
end

#ancestry_exclude_selfObject

Validate that the ancestors don’t include itself



4
5
6
# File 'lib/ancestry/instance_methods.rb', line 4

def ancestry_exclude_self
  errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.name.humanize)) if ancestor_ids.include? self.id
end

#apply_orphan_strategyObject

Apply orphan strategy (before destroy - no changes)



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
# File 'lib/ancestry/instance_methods.rb', line 24

def apply_orphan_strategy
  if !ancestry_callbacks_disabled? && !new_record?
    case self.ancestry_base_class.orphan_strategy
    when :rootify # make all children root if orphan strategy is rootify
      unscoped_descendants.each do |descendant|
        descendant.without_ancestry_callbacks do
          descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
        end
      end
    when :destroy # destroy all descendants if orphan strategy is destroy
      unscoped_descendants.each do |descendant|
        descendant.without_ancestry_callbacks do
          descendant.destroy
        end
      end
    when :adopt # make child elements of this node, child of its parent
      descendants.each do |descendant|
        descendant.without_ancestry_callbacks do
          descendant.update_attribute :ancestor_ids, (descendant.ancestor_ids.delete_if { |x| x == self.id })
        end
      end
    when :restrict # throw an exception if it has children
      raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
    end
  end
end

#cache_depthObject



148
149
150
# File 'lib/ancestry/instance_methods.rb', line 148

def cache_depth
  write_attribute self.ancestry_base_class.depth_cache_column, depth
end

#child_idsObject



214
215
216
# File 'lib/ancestry/instance_methods.rb', line 214

def child_ids
  children.pluck(self.ancestry_base_class.primary_key)
end

#child_of?(node) ⇒ Boolean

Returns:

  • (Boolean)


228
229
230
# File 'lib/ancestry/instance_methods.rb', line 228

def child_of?(node)
  self.parent_id == node.id
end

#childrenObject

Children



210
211
212
# File 'lib/ancestry/instance_methods.rb', line 210

def children
  self.ancestry_base_class.children_of(self)
end

#decrease_parent_counter_cacheObject



68
69
70
71
72
73
74
75
76
77
78
# File 'lib/ancestry/instance_methods.rb', line 68

def decrease_parent_counter_cache
  # @_trigger_destroy_callback comes from activerecord, which makes sure only once decrement when concurrent deletion.
  # but @_trigger_destroy_callback began after [email protected].
  # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L340
  # https://github.com/rails/rails/pull/14735
  # https://github.com/rails/rails/pull/27248
  return if defined?(@_trigger_destroy_callback) && !@_trigger_destroy_callback
  return if ancestry_callbacks_disabled?

  self.ancestry_base_class.decrement_counter counter_cache_column, parent_id
end

#depthObject



144
145
146
# File 'lib/ancestry/instance_methods.rb', line 144

def depth
  ancestor_ids.size
end

#descendant_ids(depth_options = {}) ⇒ Object



263
264
265
# File 'lib/ancestry/instance_methods.rb', line 263

def descendant_ids depth_options = {}
  descendants(depth_options).pluck(self.ancestry_base_class.primary_key)
end

#descendant_of?(node) ⇒ Boolean

Returns:

  • (Boolean)


267
268
269
# File 'lib/ancestry/instance_methods.rb', line 267

def descendant_of?(node)
  ancestor_ids.include?(node.id)
end

#descendants(depth_options = {}) ⇒ Object

Descendants



259
260
261
# File 'lib/ancestry/instance_methods.rb', line 259

def descendants depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
end

#has_children?Boolean Also known as: children?

Returns:

  • (Boolean)


218
219
220
# File 'lib/ancestry/instance_methods.rb', line 218

def has_children?
  self.children.exists?
end

#has_parent?Boolean Also known as: ancestors?

Ancestors

Returns:

  • (Boolean)


94
95
96
# File 'lib/ancestry/instance_methods.rb', line 94

def has_parent?
  ancestor_ids.present?
end

#has_siblings?Boolean Also known as: siblings?

Returns:

  • (Boolean)


243
244
245
# File 'lib/ancestry/instance_methods.rb', line 243

def has_siblings?
  self.siblings.count > 1
end

#increase_parent_counter_cacheObject

Counter Cache



64
65
66
# File 'lib/ancestry/instance_methods.rb', line 64

def increase_parent_counter_cache
  self.ancestry_base_class.increment_counter counter_cache_column, parent_id
end

#indirect_ids(depth_options = {}) ⇒ Object



277
278
279
# File 'lib/ancestry/instance_methods.rb', line 277

def indirect_ids depth_options = {}
  indirects(depth_options).pluck(self.ancestry_base_class.primary_key)
end

#indirect_of?(node) ⇒ Boolean

Returns:

  • (Boolean)


281
282
283
# File 'lib/ancestry/instance_methods.rb', line 281

def indirect_of?(node)
  ancestor_ids[0..-2].include?(node.id)
end

#indirects(depth_options = {}) ⇒ Object

Indirects



273
274
275
# File 'lib/ancestry/instance_methods.rb', line 273

def indirects depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
end

#is_childless?Boolean Also known as: childless?

Returns:

  • (Boolean)


223
224
225
# File 'lib/ancestry/instance_methods.rb', line 223

def is_childless?
  !has_children?
end

#is_only_child?Boolean Also known as: only_child?

Returns:

  • (Boolean)


248
249
250
# File 'lib/ancestry/instance_methods.rb', line 248

def is_only_child?
  !has_siblings?
end

#is_root?Boolean Also known as: root?

Returns:

  • (Boolean)


199
200
201
# File 'lib/ancestry/instance_methods.rb', line 199

def is_root?
  !has_parent?
end

#parentObject



173
174
175
176
177
178
179
# File 'lib/ancestry/instance_methods.rb', line 173

def parent
  if has_parent?
    unscoped_where do |scope|
      scope.find_by scope.primary_key => parent_id
    end
  end
end

#parent=(parent) ⇒ Object

currently parent= does not work in after save callbacks assuming that parent hasn’t changed



160
161
162
# File 'lib/ancestry/instance_methods.rb', line 160

def parent= parent
  self.ancestor_ids = parent ? parent.path_ids : []
end

#parent_idObject



168
169
170
# File 'lib/ancestry/instance_methods.rb', line 168

def parent_id
  ancestor_ids.last if has_parent?
end

#parent_id=(new_parent_id) ⇒ Object



164
165
166
# File 'lib/ancestry/instance_methods.rb', line 164

def parent_id= new_parent_id
  self.parent = new_parent_id.present? ? unscoped_find(new_parent_id) : nil
end

#parent_of?(node) ⇒ Boolean

Returns:

  • (Boolean)


181
182
183
# File 'lib/ancestry/instance_methods.rb', line 181

def parent_of?(node)
  self.id == node.parent_id
end

#path(depth_options = {}) ⇒ Object



140
141
142
# File 'lib/ancestry/instance_methods.rb', line 140

def path depth_options = {}
  self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
end

#path_idsObject



128
129
130
# File 'lib/ancestry/instance_methods.rb', line 128

def path_ids
  ancestor_ids + [id]
end

#path_ids_before_last_saveObject



132
133
134
# File 'lib/ancestry/instance_methods.rb', line 132

def path_ids_before_last_save
  ancestor_ids_before_last_save + [id]
end

#path_ids_in_databaseObject



136
137
138
# File 'lib/ancestry/instance_methods.rb', line 136

def path_ids_in_database
  ancestor_ids_in_database + [id]
end

#rootObject



191
192
193
194
195
196
197
# File 'lib/ancestry/instance_methods.rb', line 191

def root
  if has_parent?
    unscoped_where { |scope| scope.find_by(scope.primary_key => root_id) } || self
  else
    self
  end
end

#root_idObject

Root



187
188
189
# File 'lib/ancestry/instance_methods.rb', line 187

def root_id
  has_parent? ? ancestor_ids.first : id
end

#root_of?(node) ⇒ Boolean

Returns:

  • (Boolean)


204
205
206
# File 'lib/ancestry/instance_methods.rb', line 204

def root_of?(node)
  self.id == node.root_id
end

#sane_ancestor_ids?Boolean

Returns:

  • (Boolean)


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/ancestry/instance_methods.rb', line 106

def sane_ancestor_ids?
  current_context, self.validation_context = validation_context, nil
  errors.clear

  attribute = ancestry_base_class.ancestry_column
  ancestry_value = send(attribute)
  return true unless ancestry_value

  self.class.validators_on(attribute).each do |validator|
    validator.validate_each(self, attribute, ancestry_value)
  end
  ancestry_exclude_self
  errors.none?
ensure
  self.validation_context = current_context
end

#sibling_idsObject

NOTE: includes self



239
240
241
# File 'lib/ancestry/instance_methods.rb', line 239

def sibling_ids
  siblings.pluck(self.ancestry_base_class.primary_key)
end

#sibling_of?(node) ⇒ Boolean

Returns:

  • (Boolean)


253
254
255
# File 'lib/ancestry/instance_methods.rb', line 253

def sibling_of?(node)
  self.ancestor_ids == node.ancestor_ids
end

#siblingsObject

Siblings



234
235
236
# File 'lib/ancestry/instance_methods.rb', line 234

def siblings
  self.ancestry_base_class.siblings_of(self)
end

#subtree(depth_options = {}) ⇒ Object

Subtree



287
288
289
# File 'lib/ancestry/instance_methods.rb', line 287

def subtree depth_options = {}
  self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
end

#subtree_ids(depth_options = {}) ⇒ Object



291
292
293
# File 'lib/ancestry/instance_methods.rb', line 291

def subtree_ids depth_options = {}
  subtree(depth_options).pluck(self.ancestry_base_class.primary_key)
end

#touch_ancestors_callbackObject

Touch each of this record’s ancestors (after save)



52
53
54
55
56
57
58
59
60
61
# File 'lib/ancestry/instance_methods.rb', line 52

def touch_ancestors_callback
  if !ancestry_callbacks_disabled? && self.ancestry_base_class.touch_ancestors
    # Touch each of the old *and* new ancestors
    unscoped_current_and_previous_ancestors.each do |ancestor|
      ancestor.without_ancestry_callbacks do
        ancestor.touch
      end
    end
  end
end

#update_descendants_with_new_ancestryObject

Update descendants with new ancestry (after update)



9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/ancestry/instance_methods.rb', line 9

def update_descendants_with_new_ancestry
  # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
  if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
    # ... for each descendant ...
    unscoped_descendants_before_save.each do |descendant|
      # ... replace old ancestry with new ancestry
      descendant.without_ancestry_callbacks do
        new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_before_last_save)
        descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
      end
    end
  end
end

#update_parent_counter_cacheObject



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/ancestry/instance_methods.rb', line 80

def update_parent_counter_cache
  changed = saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)

  return unless changed

  if parent_id_was = parent_id_before_last_save
    self.ancestry_base_class.decrement_counter counter_cache_column, parent_id_was
  end

  parent_id && increase_parent_counter_cache
end

#without_ancestry_callbacksObject

Callback disabling



297
298
299
300
301
302
# File 'lib/ancestry/instance_methods.rb', line 297

def without_ancestry_callbacks
  @disable_ancestry_callbacks = true
  yield
ensure
  @disable_ancestry_callbacks = false
end