Module: Models::Lineage

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

Defined Under Namespace

Modules: ClassMethods

Instance Method Summary collapse

Instance Method Details

#ancestorsObject



161
162
163
164
# File 'app/concerns/models/lineage.rb', line 161

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

#ancestors_idsObject



143
144
145
# File 'app/concerns/models/lineage.rb', line 143

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



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

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

#descendantsObject



151
152
153
154
# File 'app/concerns/models/lineage.rb', line 151

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

#descendants_idsObject



135
136
137
# File 'app/concerns/models/lineage.rb', line 135

def descendants_ids
  self.class.descendants_ids(id)
end

#ensure_non_recursive_lineageObject (protected)



244
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
# File 'app/concerns/models/lineage.rb', line 244

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



194
195
196
# File 'app/concerns/models/lineage.rb', line 194

def family_members
  self.class.descendants_lin(root_id)
end

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



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

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

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



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

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

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



215
216
217
218
# File 'app/concerns/models/lineage.rb', line 215

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



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

def lineage_array(scope: nil, instance_method: nil)
  instance_method ||= :name
  l = []
  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



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

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

#rootObject



127
128
129
# File 'app/concerns/models/lineage.rb', line 127

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

#root_idObject

Returns the root node of the tree.



123
124
125
# File 'app/concerns/models/lineage.rb', line 123

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

#self_ancestors_and_descendantsObject



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

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



207
208
209
# File 'app/concerns/models/lineage.rb', line 207

def self_ancestors_and_descendants_ids
  ancestors_ids + [id] + descendants_ids
end

#self_and_ancestorsObject



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

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



147
148
149
# File 'app/concerns/models/lineage.rb', line 147

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]



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

def self_and_children
  [self] + children
end

#self_and_descendantsObject



156
157
158
159
# File 'app/concerns/models/lineage.rb', line 156

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



139
140
141
# File 'app/concerns/models/lineage.rb', line 139

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

#self_and_siblingsObject

Returns all siblings and a reference to the current node.

subchild1.self_and_siblings # => [subchild1, subchild2]



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

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

#self_and_siblings_idsObject



178
179
180
# File 'app/concerns/models/lineage.rb', line 178

def self_and_siblings_ids
  parent&.children&.pluck(:id) || self.class.roots.pluck(:id)
end

#siblingsObject



131
132
133
# File 'app/concerns/models/lineage.rb', line 131

def siblings
  self_and_siblings - [self]
end

#siblings_idsObject



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

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