summaryrefslogtreecommitdiff
path: root/app/models/concerns/nested_tree.rb
blob: 9a2eb0ee5c038c5071ac67c167e254ee30726012 (plain)
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
# app/models/concerns/nested_tree.rb
#
# Minimal parent_id-based replacement for the tree-traversal subset of
# awesome_nested_set actually used by this app (descendants, ancestors,
# level, root, move_to_child_of)
module NestedTree
  extend ActiveSupport::Concern

  included do
    belongs_to :parent, class_name: name, foreign_key: :parent_id, optional: true, inverse_of: :children
    has_many :children, -> { order(:id) }, class_name: name, foreign_key: :parent_id, inverse_of: :parent

    before_destroy :delete_descendants
  end

  class_methods do
    def root
      roots.first
    end

    def roots
      where(parent_id: nil)
    end

    def valid?
      # Every non-root row's parent_id must point at a real, existing row.
      where.not(parent_id: nil).where.missing(:parent).none?
    end
  end

  def root?
    parent_id.nil?
  end

  def ancestors
    result = []
    current = parent
    while current
      result << current
      current = current.parent
    end
    result
  end

  def level
    ancestors.size
  end

  def descendants
    ids = []
    queue = self.class.where(parent_id: id).pluck(:id)
    until queue.empty?
      ids.concat(queue)
      queue = self.class.where(parent_id: queue).pluck(:id)
    end
    self.class.where(id: ids)
  end

  def self_and_descendants
    self.class.where(id: [id] + descendants.pluck(:id))
  end

  def self_and_descendants_ordered_with_level
    nodes = [self] + descendants.to_a
    children_by_parent = nodes.group_by(&:parent_id)
    children_by_parent.each_value { |list| list.sort_by!(&:id) }

    result = []
    visit = ->(node, level) do
      result << [node, level]
      (children_by_parent[node.id] || []).each { |child| visit.call(child, level + 1) }
    end
    visit.call(self, 0)
    result
  end

  def move_to_child_of(new_parent)
    update!(parent_id: new_parent.id)
  end

  private

  def delete_descendants
    self.class.where(id: descendants.pluck(:id)).delete_all
  end
end