summaryrefslogtreecommitdiff
path: root/app/models/concerns/nested_tree.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/concerns/nested_tree.rb')
-rw-r--r--app/models/concerns/nested_tree.rb78
1 files changed, 78 insertions, 0 deletions
diff --git a/app/models/concerns/nested_tree.rb b/app/models/concerns/nested_tree.rb
new file mode 100644
index 0000000..befcdb3
--- /dev/null
+++ b/app/models/concerns/nested_tree.rb
@@ -0,0 +1,78 @@
1# app/models/concerns/nested_tree.rb
2#
3# Minimal parent_id-based replacement for the tree-traversal subset of
4# awesome_nested_set actually used by this app (descendants, ancestors,
5# level, root, move_to_child_of)
6module NestedTree
7 extend ActiveSupport::Concern
8
9 included do
10 belongs_to :parent, class_name: name, foreign_key: :parent_id, optional: true, inverse_of: :children
11 has_many :children, -> { order(:id) }, class_name: name, foreign_key: :parent_id, inverse_of: :parent
12 end
13
14 class_methods do
15 def root
16 roots.first
17 end
18
19 def roots
20 where(parent_id: nil)
21 end
22
23 def valid?
24 # Every non-root row's parent_id must point at a real, existing row.
25 where.not(parent_id: nil).where.missing(:parent).none?
26 end
27 end
28
29 def root?
30 parent_id.nil?
31 end
32
33 def ancestors
34 result = []
35 current = parent
36 while current
37 result << current
38 current = current.parent
39 end
40 result
41 end
42
43 def level
44 ancestors.size
45 end
46
47 def descendants
48 ids = []
49 queue = self.class.where(parent_id: id).pluck(:id)
50 until queue.empty?
51 ids.concat(queue)
52 queue = self.class.where(parent_id: queue).pluck(:id)
53 end
54 self.class.where(id: ids)
55 end
56
57 def self_and_descendants
58 self.class.where(id: [id] + descendants.pluck(:id))
59 end
60
61 def self_and_descendants_ordered_with_level
62 nodes = [self] + descendants.to_a
63 children_by_parent = nodes.group_by(&:parent_id)
64 children_by_parent.each_value { |list| list.sort_by!(&:id) }
65
66 result = []
67 visit = ->(node, level) do
68 result << [node, level]
69 (children_by_parent[node.id] || []).each { |child| visit.call(child, level + 1) }
70 end
71 visit.call(self, 0)
72 result
73 end
74
75 def move_to_child_of(new_parent)
76 update!(parent_id: new_parent.id)
77 end
78end