Module: Models::Lineage

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

Overview

ActiveSupport::Concern mixin: lineage.

Defined Under Namespace

Modules: ClassMethods

Instance Method Summary collapse

Instance Method Details

#ancestorsActiveRecord::Relation

Returns all ancestors of this node, ordered by tree depth.

Returns:

  • (ActiveRecord::Relation)

    ancestor records



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

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

#ancestors_idsArray<Integer>

Returns the ids of all ancestors of this node, at any depth.

Returns:

  • (Array<Integer>)

    ancestor ids



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

def ancestors_ids
  self.class.ancestors_ids(id)
end

#children_and_roots(klass = self.class) ⇒ Array<ApplicationRecord>

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

Parameters:

  • klass (Class) (defaults to: self.class)

    model class to fetch roots from (default: self.class)

Returns:



289
290
291
292
293
294
# File 'app/concerns/models/lineage.rb', line 289

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

#descendantsActiveRecord::Relation

Returns all descendants of this node, ordered by tree depth.

Returns:

  • (ActiveRecord::Relation)

    descendant records



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

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

#descendants_idsArray<Integer>

Returns the ids of all descendants of this node, at any depth.

Returns:

  • (Array<Integer>)

    descendant ids



189
190
191
# File 'app/concerns/models/lineage.rb', line 189

def descendants_ids
  self.class.descendants_ids(id)
end

#ensure_non_recursive_lineagevoid (protected)

This method returns an undefined value.

Validation: rejects parent assignments that would create a circular reference.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'app/concerns/models/lineage.rb', line 366

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_membersActiveRecord::Relation

Returns all descendants of the root

Returns:

  • (ActiveRecord::Relation)

    every member of this node's tree



281
282
283
# File 'app/concerns/models/lineage.rb', line 281

def family_members
  self.class.descendants_lin(root_id)
end

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

Returns the full lineage path as the record's full name.

Parameters:

  • scope (Symbol, nil) (defaults to: nil)

    optional scope applied to the ancestors before rendering

  • instance_method (Symbol, nil) (defaults to: nil)

    method called on each node for its label (default: :name)

Returns:

  • (String)

    the joined lineage path



348
349
350
# File 'app/concerns/models/lineage.rb', line 348

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

#generate_full_name_array(scope: nil, instance_method: nil) ⇒ Array<String>

Returns the full lineage path as an array of name segments.

Parameters:

  • scope (Symbol, nil) (defaults to: nil)

    optional scope applied to the ancestors before rendering

  • instance_method (Symbol, nil) (defaults to: nil)

    method called on each node for its label (default: :name)

Returns:

  • (Array<String>)

    labels from the root down to this node



357
358
359
# File 'app/concerns/models/lineage.rb', line 357

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

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

Returns the full lineage path of this node as a joined string.

Parameters:

  • separator (String) (defaults to: ' > ')

    string placed between each node label (default: ' > ')

  • scope (Symbol, nil) (defaults to: nil)

    optional scope applied to the ancestors before rendering

  • instance_method (Symbol, nil) (defaults to: nil)

    method called on each node for its label (default: :name)

Returns:

  • (String)

    the joined lineage path



316
317
318
319
# File 'app/concerns/models/lineage.rb', line 316

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) ⇒ Array<String>

Returns the full lineage path of this node as an array of labels.

Parameters:

  • scope (Symbol, nil) (defaults to: nil)

    optional scope applied to the ancestors before rendering

  • instance_method (Symbol, nil) (defaults to: nil)

    method called on each node for its label (default: :name)

Returns:

  • (Array<String>)

    labels from the root down to this node



326
327
328
329
330
331
332
333
# File 'app/concerns/models/lineage.rb', line 326

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) ⇒ String

Returns the full lineage path joined with a simple dash separator.

Parameters:

  • instance_method (Symbol, nil) (defaults to: nil)

    method called on each node for its label (default: :name)

Returns:

  • (String)

    the dash-joined lineage path



339
340
341
# File 'app/concerns/models/lineage.rb', line 339

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

#rootApplicationRecord?

Returns the root record of the tree containing this node.

Returns:



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

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

#root_idInteger?

Returns the root node of the tree.

Returns:

  • (Integer, nil)

    id of the root record



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

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

#self_ancestors_and_descendantsActiveRecord::Relation

Returns this node, all its ancestors, and all its descendants.

Returns:

  • (ActiveRecord::Relation)

    self, ancestor, and descendant records



306
307
308
# File 'app/concerns/models/lineage.rb', line 306

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

#self_ancestors_and_descendants_idsArray<Integer>

Return self and all ancestors and all descendants

Returns:

  • (Array<Integer>)

    own id, ancestor ids, and descendant ids



299
300
301
# File 'app/concerns/models/lineage.rb', line 299

def self_ancestors_and_descendants_ids
  ancestors_ids + [id] + descendants_ids
end

#self_and_ancestorsActiveRecord::Relation

Returns this node and all its ancestors, ordered by tree depth.

Returns:

  • (ActiveRecord::Relation)

    self and ancestor records



241
242
243
244
# File 'app/concerns/models/lineage.rb', line 241

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_idsArray<Integer>

Returns this node's id plus the ids of all its ancestors.

Returns:

  • (Array<Integer>)

    own id and ancestor ids



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

def self_and_ancestors_ids
  [id] + ancestors_ids
end

#self_and_childrenArray<ApplicationRecord>

Returns children (without subchildren) and current node itself.

root.self_and_children # => [root, child1]

Returns:



274
275
276
# File 'app/concerns/models/lineage.rb', line 274

def self_and_children
  [self] + children
end

#self_and_descendantsActiveRecord::Relation

Returns this node and all its descendants, ordered by tree depth.

Returns:

  • (ActiveRecord::Relation)

    self and descendant records



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

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_idsArray<Integer>

Returns this node's id plus the ids of all its descendants.

Returns:

  • (Array<Integer>)

    own id and descendant ids



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

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

#self_and_siblingsArray<ApplicationRecord>, ActiveRecord::Relation

Returns all siblings and a reference to the current node.

subchild1.self_and_siblings # => [subchild1, subchild2]

Returns:

  • (Array<ApplicationRecord>, ActiveRecord::Relation)

    sibling records including self



251
252
253
# File 'app/concerns/models/lineage.rb', line 251

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

#self_and_siblings_idsArray<Integer>

Returns the ids of all siblings including this node.

Returns:

  • (Array<Integer>)

    sibling ids including own id



258
259
260
# File 'app/concerns/models/lineage.rb', line 258

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

#siblingsArray<ApplicationRecord>

Returns the siblings of this node (nodes sharing the same parent), excluding self.

Returns:



182
183
184
# File 'app/concerns/models/lineage.rb', line 182

def siblings
  self_and_siblings - [self]
end

#siblings_idsArray<Integer>

Returns the ids of all siblings excluding this node.

Returns:

  • (Array<Integer>)

    sibling ids



265
266
267
# File 'app/concerns/models/lineage.rb', line 265

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