Module: Models::Lineage

Extended by:
ActiveSupport::Concern
Included in:
Article, Catalog, Communication, Employee, ItemDemandForecastAddition, LedgerAccount, LedgerProject, LineItem, Order, Quote, RmaItem, Role
Defined in:
app/concerns/models/lineage.rb

Overview

ActiveSupport::Concern mixin: lineage.

Defined Under Namespace

Modules: ClassMethods

Instance Method Summary collapse

Instance Method Details

#ancestorsObject



163
164
165
166
# File 'app/concerns/models/lineage.rb', line 163

def ancestors
  ids = ancestors_ids
  self.class.where(id: ancestors_ids).order(self.class.generate_order_by(ids))
end

#ancestors_idsObject



145
146
147
# File 'app/concerns/models/lineage.rb', line 145

def ancestors_ids
  self.class.ancestors_ids(id)
end

#children_and_roots(klass = self.class) ⇒ Object

Returns all children of the node and all roots, but removes the current node and its root



201
202
203
204
205
206
# File 'app/concerns/models/lineage.rb', line 201

def children_and_roots(klass = self.class)
  available = children + klass.roots
  available.delete(self)
  available.delete(root)
  available
end

#descendantsObject



153
154
155
156
# File 'app/concerns/models/lineage.rb', line 153

def descendants
  ids = descendants_ids
  self.class.where(id: ids).order(self.class.generate_order_by(ids))
end

#descendants_idsObject



137
138
139
# File 'app/concerns/models/lineage.rb', line 137

def descendants_ids
  self.class.descendants_ids(id)
end

#ensure_non_recursive_lineageObject (protected)



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'app/concerns/models/lineage.rb', line 245

def ensure_non_recursive_lineage
  # Dynamically determine the foreign key from the :children association
  fk = self.class.reflect_on_association(:children)&.foreign_key || :parent_id

  # Guard: Skip if this model doesn't have the foreign key attribute
  # This can happen if a subclass doesn't use lineage but inherits the concern
  return unless respond_to?(fk)

  parent_value = send(fk)

  return if parent_value.blank?

  # When parent changes, we need to check if the NEW parent (or any of its ancestors)
  # is a descendant of self. This would create a circular reference.
  #
  # For example: If A > B > C exists, and we try to set A.parent = C,
  # we'd get C > A > B > C (circular!)
  #
  # We check by walking up from the NEW parent to see if we encounter self

  visited = Set.new([id])
  current_parent_id = parent_value

  while current_parent_id.present?
    if current_parent_id == id
      errors.add(fk, 'would create a circular reference - cannot be a descendant of itself')
      break
    end

    break if visited.include?(current_parent_id) # Prevent infinite loop

    visited.add(current_parent_id)

    # Look up the parent's parent (not using association to avoid caching issues)
    current_parent_id = self.class.where(id: current_parent_id).pick(fk)
  end
end

#family_membersObject

Returns all descendants of the root



196
197
198
# File 'app/concerns/models/lineage.rb', line 196

def family_members
  self.class.descendants_lin(root_id)
end

#generate_full_name(scope: nil, instance_method: nil) ⇒ Object



235
236
237
# File 'app/concerns/models/lineage.rb', line 235

def generate_full_name(scope: nil, instance_method: nil)
  lineage(scope:, instance_method:)
end

#generate_full_name_array(scope: nil, instance_method: nil) ⇒ Object



239
240
241
# File 'app/concerns/models/lineage.rb', line 239

def generate_full_name_array(scope: nil, instance_method: nil)
  lineage_array(scope:, instance_method:)
end

#lineage(separator: ' > ', scope: nil, instance_method: nil) ⇒ Object



217
218
219
220
# File 'app/concerns/models/lineage.rb', line 217

def lineage(separator: ' > ', scope: nil, instance_method: nil)
  l = lineage_array(scope:, instance_method:)
  l.join(separator)
end

#lineage_array(scope: nil, instance_method: nil) ⇒ Object



222
223
224
225
226
227
228
229
# File 'app/concerns/models/lineage.rb', line 222

def lineage_array(scope: nil, instance_method: nil)
  instance_method ||= :name
  lines = ancestors
  lines = lines.send(scope) if scope
  lines = lines.map { |l| l.send(instance_method) }
  lines = lines.reverse
  lines << send(instance_method)
end

#lineage_simple(instance_method: nil) ⇒ Object



231
232
233
# File 'app/concerns/models/lineage.rb', line 231

def lineage_simple(instance_method: nil)
  lineage(separator: '-', instance_method:)
end

#rootObject



129
130
131
# File 'app/concerns/models/lineage.rb', line 129

def root
  self.class.find(root_id) if root_id
end

#root_idObject

Returns the root node of the tree.



125
126
127
# File 'app/concerns/models/lineage.rb', line 125

def root_id
  self.class.root_ids(id).first
end

#self_ancestors_and_descendantsObject



213
214
215
# File 'app/concerns/models/lineage.rb', line 213

def self_ancestors_and_descendants
  self.class.where(id: self_ancestors_and_descendants_ids)
end

#self_ancestors_and_descendants_idsObject

Return self and all ancestors and all descendants



209
210
211
# File 'app/concerns/models/lineage.rb', line 209

def self_ancestors_and_descendants_ids
  ancestors_ids + [id] + descendants_ids
end

#self_and_ancestorsObject



168
169
170
171
# File 'app/concerns/models/lineage.rb', line 168

def self_and_ancestors
  ids = self_and_ancestors_ids
  self.class.where(id: ids).order(self.class.generate_order_by(ids))
end

#self_and_ancestors_idsObject



149
150
151
# File 'app/concerns/models/lineage.rb', line 149

def self_and_ancestors_ids
  [id] + ancestors_ids
end

#self_and_childrenObject

Returns children (without subchildren) and current node itself.

root.self_and_children # => [root, child1]



191
192
193
# File 'app/concerns/models/lineage.rb', line 191

def self_and_children
  [self] + children
end

#self_and_descendantsObject



158
159
160
161
# File 'app/concerns/models/lineage.rb', line 158

def self_and_descendants
  ids = self_and_descendants_ids
  self.class.where(id: ids).order(self.class.generate_order_by(ids))
end

#self_and_descendants_idsObject



141
142
143
# File 'app/concerns/models/lineage.rb', line 141

def self_and_descendants_ids
  ([id] + descendants_ids).filter_map(&:presence)
end

#self_and_siblingsObject

Returns all siblings and a reference to the current node.

subchild1.self_and_siblings # => [subchild1, subchild2]



176
177
178
# File 'app/concerns/models/lineage.rb', line 176

def self_and_siblings
  parent ? parent.children : self.class.roots
end

#self_and_siblings_idsObject



180
181
182
# File 'app/concerns/models/lineage.rb', line 180

def self_and_siblings_ids
  parent&.children&.ids || self.class.roots.ids
end

#siblingsObject



133
134
135
# File 'app/concerns/models/lineage.rb', line 133

def siblings
  self_and_siblings - [self]
end

#siblings_idsObject



184
185
186
# File 'app/concerns/models/lineage.rb', line 184

def siblings_ids
  self_and_siblings_ids.reject { |id| id == self.id }
end