Skip to content

Commit 468d5b0

Browse files
tchakclaude
andcommitted
tech: add ancestors, parent, section and repetition to types de champ and champs
TypeDeChampBase#ancestors(revision) returns the sections and repetitions above a type de champ, outermost first; parent, section and repetition derive from it. Champ#ancestors projects them with the champ's row_id inside the repetition, and exposes the same derived accessors. Champ#parent changes semantics: it used to return the parent repetition TypeDeChamp, it now returns the nearest container as a projected champ. Existing callers wanted the repetition and now use Champ#repetition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4619194 commit 468d5b0

11 files changed

Lines changed: 168 additions & 59 deletions

File tree

app/components/dossiers/errors_full_messages_component.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def model_libelle(model)
3636
def parent_prefix(model)
3737
return "" if !model.child?
3838

39-
"#{[is_in_fieldset?(model) ? "[#{row_number(model)}]" : nil, model.parent.libelle].compact.join(" ")} - "
39+
"#{[is_in_fieldset?(model) ? "[#{row_number(model)}]" : nil, model.repetition.libelle].compact.join(" ")} - "
4040
end
4141

4242
def row_number_prefix(model)
@@ -48,13 +48,13 @@ def row_number_prefix(model)
4848
def row_number(model)
4949
return 1 if !model.child?
5050

51-
model.dossier.repetition_row_ids(model.parent).index(model.row_id) + 1
51+
model.repetition.row_ids.index(model.row_id) + 1
5252
end
5353

5454
def is_in_fieldset?(model)
5555
return 0 if !model.child?
5656

57-
model.dossier.revision.children_of(model.parent).size > 1
57+
model.dossier.revision.children_of(model.repetition.type_de_champ).size > 1
5858
end
5959

6060
def render?

app/components/editable_champ/editable_champ_component.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ def champ_component
1717
end
1818

1919
def parent_fieldset_legend_id
20-
"#{@champ.parent.html_id}-legend"
20+
"#{@champ.repetition.html_id}-legend"
2121
end
2222

2323
def fieldset_legend_id
24-
"#{@champ.parent.html_id(@champ.row_id)}-legend"
24+
"#{@champ.repetition.type_de_champ.html_id(@champ.row_id)}-legend"
2525
end
2626

2727
def aria_labelledby_prefix
@@ -33,17 +33,13 @@ def aria_labelledby_prefix
3333
def number_of_siblings_if_in_repetition
3434
return if !@champ.child?
3535

36-
@number_of_siblings_if_in_repetition ||= @champ.dossier.revision.children_of(@champ.parent).count
36+
@number_of_siblings_if_in_repetition ||= @champ.dossier.revision.children_of(@champ.repetition.type_de_champ).count
3737
end
3838

3939
def row_number_if_in_repetition
4040
return if !@champ.child? || number_of_siblings_if_in_repetition > 1
4141

42-
@row_number_if_in_repetition ||= begin
43-
parent = @champ.parent
44-
row_ids = @champ.dossier.repetition_row_ids(parent)
45-
row_ids.find_index(@champ.row_id)&.+ 1
46-
end
42+
@row_number_if_in_repetition ||= @champ.repetition.row_ids.find_index(@champ.row_id)&.+ 1
4743
end
4844

4945
private

app/models/champ.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,23 @@ def child?
131131
row_id.present? && !is_type?(TypeDeChamp.type_champs.fetch(:repetition))
132132
end
133133

134-
def parent
135-
return nil if row_id.blank?
136-
137-
dossier.revision.parent_of(type_de_champ)
134+
# Sections and repetitions above this champ in the tree, outermost first.
135+
# Ancestors inside the repetition share the champ's row_id.
136+
def ancestors
137+
@ancestors ||= begin
138+
row = nil
139+
type_de_champ.ancestors(dossier.revision).map do |tdc|
140+
ancestor = dossier.project_champ(tdc, row_id: row)
141+
row = row_id if tdc.repetition?
142+
ancestor
143+
end
144+
end
138145
end
139146

147+
def parent = ancestors.last
148+
def section = ancestors.reverse.find(&:header_section?)
149+
def repetition = ancestors.find(&:repetition?)
150+
140151
def row?
141152
row_id.present? && is_type?(TypeDeChamp.type_champs.fetch(:repetition))
142153
end

app/models/concerns/champ_conditional_concern.rb

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,7 @@ def parent_hidden?
5555
return false if !child?
5656

5757
# otherwise maybe the champ has been moved outside a repetition
58-
parent_tdc = dossier.revision.parent_of(type_de_champ)
59-
60-
return false if parent_tdc.nil?
61-
62-
parent = dossier.project_champs
63-
.find { it.type_de_champ == parent_tdc }
64-
65-
!parent.visible?
58+
parent = repetition
59+
parent.present? && !parent.visible?
6660
end
6761
end

app/models/concerns/treeable_concern.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ def tree_roots(nodes)
2929
nodes.map { it.is_a?(Array) ? it.first : it }
3030
end
3131

32+
# Subtree ([header, *children]) rooted at the section with the given
33+
# stable_id, or nil when not found.
34+
def find_subtree(tree, stable_id)
35+
tree.each do |node|
36+
next unless node.is_a?(Array)
37+
return node if node.first.stable_id == stable_id
38+
39+
subtree = find_subtree(node, stable_id)
40+
return subtree if subtree
41+
end
42+
nil
43+
end
44+
45+
# Section headers above the node with the given stable_id, outermost first;
46+
# [] when the node is at the top of the tree, nil when not found.
47+
def find_section_ancestors(tree, stable_id)
48+
tree.each do |node|
49+
if node.is_a?(Array)
50+
return [] if node.first.stable_id == stable_id
51+
52+
ancestors = find_section_ancestors(node.drop(1), stable_id)
53+
return [node.first, *ancestors] if ancestors
54+
elsif node.stable_id == stable_id
55+
return []
56+
end
57+
end
58+
nil
59+
end
60+
3261
def to_tree(types_de_champ:)
3362
rooted_tree = []
3463
walk = Array.new(MAX_DEPTH)

app/models/type_de_champ.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class TypeDeChamp < ApplicationRecord
179179

180180
belongs_to :referentiel, optional: true, inverse_of: :types_de_champ
181181

182-
delegate :estimated_fill_duration, :estimated_read_duration, :tags_for_template, :libelles_for_export, :libelle_for_export, :primary_options, :secondary_options, :columns, :column, :canonical_column, :info_columns, :children, to: :dynamic_type
182+
delegate :estimated_fill_duration, :estimated_read_duration, :tags_for_template, :libelles_for_export, :libelle_for_export, :primary_options, :secondary_options, :columns, :column, :canonical_column, :info_columns, :children, :ancestors, :parent, :section, :repetition, to: :dynamic_type
183183

184184
class WithIndifferentAccess
185185
def self.load(options)
Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,15 @@
11
# frozen_string_literal: true
22

33
class TypesDeChamp::HeaderSectionTypeDeChamp < TypesDeChamp::TypeDeChampBase
4-
include TreeableConcern
5-
64
def tags_for_template = [].freeze
75

86
# Direct children of the section: its own types de champ and the immediate subsections
97
# (but not their content). Sections have no parent relationship in the database, so
108
# children are reconstructed from the ordered list of siblings and header levels.
119
def children(revision)
12-
subtree = find_subtree(to_tree(types_de_champ: siblings(revision)))
10+
subtree = find_subtree(to_tree(types_de_champ: siblings(revision)), stable_id)
1311
return [] if subtree.nil?
1412

15-
subtree.drop(1).map { it.is_a?(Array) ? it.first : it }
16-
end
17-
18-
private
19-
20-
def siblings(revision)
21-
parent = revision.parent_of(@type_de_champ)
22-
if parent.present?
23-
revision.children_of(parent)
24-
elsif public?
25-
revision.types_de_champ_public
26-
else
27-
revision.types_de_champ_private
28-
end
29-
end
30-
31-
def find_subtree(tree)
32-
tree.each do |node|
33-
next unless node.is_a?(Array)
34-
return node if node.first.stable_id == stable_id
35-
36-
subtree = find_subtree(node)
37-
return subtree if subtree.present?
38-
end
39-
nil
13+
tree_roots(subtree.drop(1))
4014
end
4115
end

app/models/types_de_champ/repetition_type_de_champ.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# frozen_string_literal: true
22

33
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
4-
include TreeableConcern
5-
64
# Direct children of the repetition: its own types de champ and top-level sections
75
# (but not the content of those sections).
86
def children(revision)

app/models/types_de_champ/type_de_champ_base.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
class TypesDeChamp::TypeDeChampBase
44
include ActiveModel::Validations
5+
include TreeableConcern
56

67
delegate :description, :libelle, :mandatory, :mandatory?, :stable_id, :fillable?, :public?, :type_champ, :options_for_select, :drop_down_options, :drop_down_other?, :drop_down_advanced?, :referentiel, :rib?, :justificatif_domicile?, :avis_impot?, :titre_identite?, :ocr_compatible?, to: :@type_de_champ
78

@@ -18,6 +19,23 @@ def initialize(type_de_champ)
1819
# have children, other types are leaves.
1920
def children(revision) = []
2021

22+
# Sections and repetitions above this type de champ in the tree, outermost
23+
# first.
24+
def ancestors(revision)
25+
repetition = revision.parent_of(@type_de_champ)
26+
sections = find_section_ancestors(to_tree(types_de_champ: siblings(revision)), stable_id) || []
27+
28+
if repetition.present?
29+
repetition.ancestors(revision) + [repetition] + sections
30+
else
31+
sections
32+
end
33+
end
34+
35+
def parent(revision) = ancestors(revision).last
36+
def section(revision) = ancestors(revision).reverse.find(&:header_section?)
37+
def repetition(revision) = ancestors(revision).find(&:repetition?)
38+
2139
def tags_for_template
2240
type_de_champ = @type_de_champ
2341
conditional = type_de_champ.condition.present?
@@ -137,6 +155,17 @@ def column(column_id)
137155

138156
private
139157

158+
def siblings(revision)
159+
parent = revision.parent_of(@type_de_champ)
160+
if parent.present?
161+
revision.children_of(parent)
162+
elsif public?
163+
revision.types_de_champ_public
164+
else
165+
revision.types_de_champ_private
166+
end
167+
end
168+
140169
def champ_text_value(champ)
141170
if champ.is_type?(TypeDeChamp.type_champs.fetch(:multiple_drop_down_list))
142171
values = TypesDeChamp::MultipleDropDownListTypeDeChamp.parse_selected_options(champ)

spec/models/champ_spec.rb

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -616,14 +616,44 @@
616616
end
617617
end
618618

619-
describe "#parent" do
620-
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :repetition, mandatory: false, children: [{ type: :text }] }]) }
619+
describe "#ancestors, #parent, #section and #repetition" do
620+
let(:procedure) do
621+
create(:procedure, types_de_champ_public: [
622+
{ type: :text, libelle: 't0' },
623+
{ type: :header_section, level: 1, libelle: 's1' },
624+
{ type: :header_section, level: 2, libelle: 's1.1' },
625+
{
626+
type: :repetition, mandatory: false, libelle: 'rep', children: [
627+
{ type: :header_section, level: 1, libelle: 'rs1' },
628+
{ type: :text, libelle: 'rt1' },
629+
],
630+
},
631+
])
632+
end
621633
let(:dossier) { create(:dossier, :with_populated_champs, procedure:) }
634+
let(:champ) { dossier.filled_champs_public.find { _1.libelle == 'rt1' } }
635+
636+
it "returns the projected champs above, outermost first" do
637+
expect(champ.ancestors.map(&:libelle)).to eq(['s1', 's1.1', 'rep', 'rs1'])
638+
expect(champ.ancestors.map(&:row_id)).to eq([nil, nil, nil, champ.row_id])
639+
end
640+
641+
it "memoizes ancestors" do
642+
expect(champ.ancestors).to equal(champ.ancestors)
643+
end
622644

623-
let(:champ) { dossier.champ_data.where(type: "Champs::TextChamp").first }
645+
it "exposes parent, section and repetition" do
646+
expect(champ.parent.libelle).to eq('rs1')
647+
expect(champ.parent.row_id).to eq(champ.row_id)
648+
expect(champ.section.libelle).to eq('rs1')
649+
expect(champ.repetition.libelle).to eq('rep')
650+
expect(champ.repetition.row_id).to be_nil
624651

625-
it "returns the parent" do
626-
expect(champ.parent).to eq(TypeDeChamp.find_by(type_champ: "repetition"))
652+
root_champ = dossier.project_champs_public.find { _1.libelle == 't0' }
653+
expect(root_champ.ancestors).to eq([])
654+
expect(root_champ.parent).to be_nil
655+
expect(root_champ.section).to be_nil
656+
expect(root_champ.repetition).to be_nil
627657
end
628658
end
629659

0 commit comments

Comments
 (0)