summaryrefslogtreecommitdiff
path: root/app/models/concerns
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-07 02:46:37 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-07 02:46:37 +0200
commit7b6af89509e8439fe2474e623ee97e4db67ab011 (patch)
tree47aa227de58d566727e8afdc44d7abc0934f22a6 /app/models/concerns
parent527376039c527eb8f559c5e6da76429bc3f3ee4f (diff)
Replace awesome_nested_set with a plain parent_id-based NestedTree concern
Root-caused this session: appending a child to any node never widened that parent's own rgt boundary, on the pinned revision (Gemfile tracked main directly, chasing a too-conservative gemspec constraint - not, as first assumed, a deliberate pin to avoid a known bug). Reproduced cleanly on a single ordinary create with no concurrency and no bulk operation involved, confirmed via the gem's own SetValidator, then confirmed as the root cause of nodes_controller_test.rb's 3 long-standing "pre-existing" failures - not three separate mysteries, one bug. admin_controller's sitemap needed its own real conversion, not just a drop-in: awesome_nested_set's lft column implicitly provided correct depth-first tree order for free, which the old code combined with a separate class-level each_with_level iterator. Both replaced by one method, self_and_descendants_ordered_with_level, computing an ordered [node, level] list in a single query-then-walk pass - checked against the actual view template first (admin/index.html.erb) rather than assumed, since it relies on list order alone to render correct visual nesting. lft/rgt/depth columns intentionally left in schema, unused - dropping them is a separate, deliberately deferred migration once this is proven running for a while, not bundled with the behavior change.
Diffstat (limited to 'app/models/concerns')
-rw-r--r--app/models/concerns/nested_tree.rb86
1 files changed, 86 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..9a2eb0e
--- /dev/null
+++ b/app/models/concerns/nested_tree.rb
@@ -0,0 +1,86 @@
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
13 before_destroy :delete_descendants
14 end
15
16 class_methods do
17 def root
18 roots.first
19 end
20
21 def roots
22 where(parent_id: nil)
23 end
24
25 def valid?
26 # Every non-root row's parent_id must point at a real, existing row.
27 where.not(parent_id: nil).where.missing(:parent).none?
28 end
29 end
30
31 def root?
32 parent_id.nil?
33 end
34
35 def ancestors
36 result = []
37 current = parent
38 while current
39 result << current
40 current = current.parent
41 end
42 result
43 end
44
45 def level
46 ancestors.size
47 end
48
49 def descendants
50 ids = []
51 queue = self.class.where(parent_id: id).pluck(:id)
52 until queue.empty?
53 ids.concat(queue)
54 queue = self.class.where(parent_id: queue).pluck(:id)
55 end
56 self.class.where(id: ids)
57 end
58
59 def self_and_descendants
60 self.class.where(id: [id] + descendants.pluck(:id))
61 end
62
63 def self_and_descendants_ordered_with_level
64 nodes = [self] + descendants.to_a
65 children_by_parent = nodes.group_by(&:parent_id)
66 children_by_parent.each_value { |list| list.sort_by!(&:id) }
67
68 result = []
69 visit = ->(node, level) do
70 result << [node, level]
71 (children_by_parent[node.id] || []).each { |child| visit.call(child, level + 1) }
72 end
73 visit.call(self, 0)
74 result
75 end
76
77 def move_to_child_of(new_parent)
78 update!(parent_id: new_parent.id)
79 end
80
81 private
82
83 def delete_descendants
84 self.class.where(id: descendants.pluck(:id)).delete_all
85 end
86end