From 9496faa1101f61e856bfb526b16feb2d9ddf2a5e Mon Sep 17 00:00:00 2001
From: Guflly <145608489+Guflly@users.noreply.github.com>
Date: Fri, 7 Aug 2026 21:35:53 -0700
Subject: [PATCH] Fix court report topic output and ordering
---
app/controllers/casa_org_controller.rb | 2 +-
.../case_court_reports_controller.rb | 5 ++-
app/models/case_court_report_context.rb | 22 ++++++----
app/services/case_contacts_contact_dates.rb | 16 ++++---
.../casa_cases/_court_report_modal.html.erb | 4 ++
.../_generate_docx.html.erb | 4 ++
spec/factories/case_court_report_context.rb | 4 +-
spec/models/case_court_report_context_spec.rb | 43 +++++++++++++++----
spec/models/case_court_report_spec.rb | 42 ++++++++++++++++++
spec/requests/casa_org_spec.rb | 9 ++++
spec/requests/case_court_reports_spec.rb | 13 ++++++
.../case_contacts_contact_dates_spec.rb | 19 ++++----
spec/system/casa_cases/show_spec.rb | 1 +
spec/system/case_court_reports/index_spec.rb | 1 +
14 files changed, 150 insertions(+), 35 deletions(-)
diff --git a/app/controllers/casa_org_controller.rb b/app/controllers/casa_org_controller.rb
index 523268a469..fd3cd9fd59 100644
--- a/app/controllers/casa_org_controller.rb
+++ b/app/controllers/casa_org_controller.rb
@@ -91,7 +91,7 @@ def set_learning_hour_topics
end
def set_contact_topics
- @contact_topics = @casa_org.contact_topics.where(soft_delete: false)
+ @contact_topics = @casa_org.contact_topics.where(soft_delete: false).order(:question)
end
def set_custom_org_links
diff --git a/app/controllers/case_court_reports_controller.rb b/app/controllers/case_court_reports_controller.rb
index a63d01f5a9..6bedbf6fff 100644
--- a/app/controllers/case_court_reports_controller.rb
+++ b/app/controllers/case_court_reports_controller.rb
@@ -65,7 +65,7 @@ def generate
private
def date_range_params
- params.permit(:time_zone, case_court_report: %i[start_date end_date])
+ params.permit(:time_zone, case_court_report: %i[start_date end_date include_empty_topics])
end
def case_params
@@ -94,7 +94,8 @@ def generate_report_to_string(casa_case, time_range)
path_to_template: template_docx_file.to_path,
time_zone: time_range[:time_zone],
start_date: time_range[:case_court_report][:start_date],
- end_date: time_range[:case_court_report][:end_date]
+ end_date: time_range[:case_court_report][:end_date],
+ include_empty_topics: time_range[:case_court_report][:include_empty_topics]
}
context = CaseCourtReportContext.new(args).context
court_report = CaseCourtReport.new(path_to_template: template_docx_file.to_path, context: context)
diff --git a/app/models/case_court_report_context.rb b/app/models/case_court_report_context.rb
index a0fce80c76..29db173a77 100644
--- a/app/models/case_court_report_context.rb
+++ b/app/models/case_court_report_context.rb
@@ -12,6 +12,7 @@ def initialize(args = {})
@path_to_template = args[:path_to_template]
@court_date = args[:court_date] || @casa_case.next_court_date
@case_court_orders = args[:case_court_orders] || @casa_case.case_court_orders
+ @include_empty_topics = ActiveModel::Type::Boolean.new.cast(args[:include_empty_topics])
@date_range = calculate_date_range(args)
end
@@ -59,9 +60,11 @@ def case_orders(orders)
def filtered_interviewees
CaseContactContactType
- .joins(:contact_type, case_contact: :casa_case)
+ .joins(contact_type: :contact_type_group, case_contact: :casa_case)
+ .includes(:case_contact, contact_type: :contact_type_group)
.where("case_contacts.casa_case_id": @casa_case.id)
.where("case_contacts.occurred_at": @date_range)
+ .order("contact_type_groups.name ASC", "contact_types.name ASC", "case_contact_contact_types.id ASC")
end
def case_details
@@ -92,15 +95,15 @@ def org_address(path_to_template)
# Sample output
#
# expected_topics = {
- # "Question 1" => {topic: "Question 1", details: "Details 1", answers: [
+ # "Question 1" => {topic: "Question 1", details: "", answers: [
# {date: "12/02/20", medium: "Type A1, Type B1", value: "Answer 1"},
# {date: "12/03/20", medium: "Type A2, Type B2", value: "Answer 3"}
# ]},
- # "Question 2" => {topic: "Question 2", details: "Details 2", answers: [
+ # "Question 2" => {topic: "Question 2", details: "", answers: [
# {date: "12/02/20", medium: "Type A1, Type B1", value: "Answer 2"},
# {date: "12/04/20", medium: "Type A3, Type B3", value: "Answer 5"}
# ]},
- # "Question 3" => {topic: "Question 3", details: "Details 3", answers: [
+ # "Question 3" => {topic: "Question 3", details: "", answers: [
# {date: "12/03/20", medium: "Type A2, Type B2", value: "No Answer Provided"},
# {date: "12/04/20", medium: "Type A3, Type B3", value: "No Answer Provided"}
# ]}
@@ -112,7 +115,7 @@ def court_topics
hash[topic.question] = {
answers: answers_by_topic_id.fetch(topic.id, []),
topic: topic.question,
- details: topic.details
+ details: ""
}
end
end
@@ -120,10 +123,13 @@ def court_topics
private
def report_topics(answered_topic_ids)
- ContactTopic
+ topics = ContactTopic
.where(casa_org: @casa_case.casa_org, exclude_from_court_report: false)
- .merge(ContactTopic.active.or(ContactTopic.where(id: answered_topic_ids)))
- .order(:id)
+ .order(:question)
+
+ return topics.where(id: answered_topic_ids) unless @include_empty_topics
+
+ topics.merge(ContactTopic.active.or(ContactTopic.where(id: answered_topic_ids)))
end
def court_topic_answers
diff --git a/app/services/case_contacts_contact_dates.rb b/app/services/case_contacts_contact_dates.rb
index 372fc68110..35a23238dd 100644
--- a/app/services/case_contacts_contact_dates.rb
+++ b/app/services/case_contacts_contact_dates.rb
@@ -4,13 +4,17 @@ def initialize(case_contact_contact_types)
end
def contact_dates_details
- contact_type_names = @case_contact_contact_types.map(&:contact_type).map(&:name).uniq # .sort # TODO sort after refactor
- contact_type_names.map do |contact_type_name|
- case_contacts = case_contacts_for_type(contact_type_name)
+ contact_types = @case_contact_contact_types
+ .map(&:contact_type)
+ .uniq
+ .sort_by { |contact_type| [contact_type.contact_type_group.name, contact_type.name] }
+
+ contact_types.map do |contact_type|
+ case_contacts = case_contacts_for_type(contact_type)
{
name: "Names of persons involved, starting with the child's name",
- type: contact_type_name,
+ type: contact_type.name,
dates: order_and_format(case_contacts),
dates_by_medium_type: case_contacts.group_by(&:medium_type).transform_values { |vals| order_and_format(vals) }
}
@@ -19,9 +23,9 @@ def contact_dates_details
private
- def case_contacts_for_type(contact_type_name)
+ def case_contacts_for_type(contact_type)
@case_contact_contact_types
- .select { |ccct| ccct.contact_type.name == contact_type_name }
+ .select { |ccct| ccct.contact_type_id == contact_type.id }
.map(&:case_contact)
end
diff --git a/app/views/casa_cases/_court_report_modal.html.erb b/app/views/casa_cases/_court_report_modal.html.erb
index 0c003a1839..8c459e19b4 100644
--- a/app/views/casa_cases/_court_report_modal.html.erb
+++ b/app/views/casa_cases/_court_report_modal.html.erb
@@ -32,6 +32,10 @@
class: "block w-full rounded-lg border border-slate-300 px-3.5 py-2.5 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+
<% end %>
<%= render Dialog::FooterComponent.new do %>
diff --git a/app/views/case_court_reports/_generate_docx.html.erb b/app/views/case_court_reports/_generate_docx.html.erb
index d6477a5359..8fe366cef7 100644
--- a/app/views/case_court_reports/_generate_docx.html.erb
+++ b/app/views/case_court_reports/_generate_docx.html.erb
@@ -57,6 +57,10 @@
data: {court_report_target: "endDate"}, class: input_class %>
+
<% end %>
<%= render Dialog::FooterComponent.new do %>
diff --git a/spec/factories/case_court_report_context.rb b/spec/factories/case_court_report_context.rb
index 2d064cbda2..0c7db84d78 100644
--- a/spec/factories/case_court_report_context.rb
+++ b/spec/factories/case_court_report_context.rb
@@ -12,6 +12,7 @@
start_date { nil }
end_date { nil }
time_zone { nil }
+ include_empty_topics { false }
end
initialize_with {
@@ -31,7 +32,8 @@
case_court_orders: case_court_orders,
start_date: start_date,
end_date: end_date,
- time_zone: time_zone
+ time_zone: time_zone,
+ include_empty_topics: include_empty_topics
)
}
end
diff --git a/spec/models/case_court_report_context_spec.rb b/spec/models/case_court_report_context_spec.rb
index ab2e3615ac..6bed463c47 100644
--- a/spec/models/case_court_report_context_spec.rb
+++ b/spec/models/case_court_report_context_spec.rb
@@ -229,7 +229,7 @@
expect(court_topics.values).to all(
a_hash_including(
topic: a_kind_of(String),
- details: a_kind_of(String),
+ details: "",
answers: all(
a_hash_including(
date: a_string_matching(/\d{2}\/\d{2}\/\d{2}/),
@@ -270,13 +270,21 @@
end
context "when some topics have no answers" do
- it "includes every topic, with an empty answer list for unanswered ones" do
+ it "omits unanswered topics by default" do
create(:contact_topic_answer, case_contact: contacts[0], contact_topic: topics[0], value: "Answer 1")
court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics
- expect(court_topics.keys).to eq(["Question 1", "Question 2", "Question 3"])
+ expect(court_topics.keys).to eq(["Question 1"])
expect(court_topics["Question 1"][:answers].pluck(:value)).to eq(["Answer 1"])
+ end
+
+ it "includes unanswered topics when requested" do
+ create(:contact_topic_answer, case_contact: contacts[0], contact_topic: topics[0], value: "Answer 1")
+
+ court_topics = build(:case_court_report_context, casa_case: casa_case, include_empty_topics: true).court_topics
+
+ expect(court_topics.keys).to eq(["Question 1", "Question 2", "Question 3"])
expect(court_topics["Question 2"][:answers]).to eq([])
expect(court_topics["Question 3"][:answers]).to eq([])
end
@@ -284,7 +292,7 @@
it "does not include unanswered topics that are inactive" do
topics[1].update!(active: false)
- court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics
+ court_topics = build(:case_court_report_context, casa_case: casa_case, include_empty_topics: true).court_topics
expect(court_topics.keys).to eq(["Question 1", "Question 3"])
end
@@ -292,7 +300,7 @@
it "does not include unanswered topics excluded from the court report" do
topics[1].update!(exclude_from_court_report: true)
- court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics
+ court_topics = build(:case_court_report_context, casa_case: casa_case, include_empty_topics: true).court_topics
expect(court_topics.keys).to eq(["Question 1", "Question 3"])
end
@@ -301,21 +309,24 @@
topics
create(:contact_topic, question: "Other Org Question")
- court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics
+ court_topics = build(:case_court_report_context, casa_case: casa_case, include_empty_topics: true).court_topics
expect(court_topics.keys).to eq(["Question 1", "Question 2", "Question 3"])
end
end
context "when answers occur in a different order than the topics were created" do
- it "orders topics by creation order, not by answer date" do
+ it "orders topics alphabetically" do
+ topics[0].update!(question: "Zulu")
+ topics[1].update!(question: "Alpha")
+ topics[2].update!(question: "Middle")
create(:contact_topic_answer, case_contact: contacts[0], contact_topic: topics[2], value: "Earliest answer")
create(:contact_topic_answer, case_contact: contacts[1], contact_topic: topics[1], value: "Middle answer")
create(:contact_topic_answer, case_contact: contacts[2], contact_topic: topics[0], value: "Latest answer")
court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics
- expect(court_topics.keys).to eq(["Question 1", "Question 2", "Question 3"])
+ expect(court_topics.keys).to eq(["Alpha", "Middle", "Zulu"])
end
end
@@ -326,7 +337,7 @@
court_topics = build(:case_court_report_context, casa_case: casa_case).court_topics
- expect(court_topics.keys).to eq(["Question 1", "Question 2", "Question 3"])
+ expect(court_topics.keys).to eq(["Question 2"])
expect(court_topics["Question 2"][:answers].pluck(:value)).to eq(["Answer before deactivation"])
end
end
@@ -400,6 +411,20 @@
expect(result).to be_empty
end
+
+ it "orders interviewees by contact type group and name" do
+ casa_case = create(:casa_case)
+ family_group = create(:contact_type_group, casa_org: casa_case.casa_org, name: "Family")
+ health_group = create(:contact_type_group, casa_org: casa_case.casa_org, name: "Health")
+ therapist = create(:contact_type, contact_type_group: health_group, name: "Therapist")
+ aunt = create(:contact_type, contact_type_group: family_group, name: "Aunt")
+ counselor = create(:contact_type, contact_type_group: health_group, name: "Counselor")
+ create(:case_contact, casa_case: casa_case, contact_types: [therapist, aunt, counselor])
+
+ result = build(:case_court_report_context, casa_case: casa_case).filtered_interviewees
+
+ expect(result.map { |interviewee| interviewee.contact_type.name }).to eq(["Aunt", "Counselor", "Therapist"])
+ end
end
describe "#context" do
diff --git a/spec/models/case_court_report_spec.rb b/spec/models/case_court_report_spec.rb
index 202211d853..7d934ffe77 100644
--- a/spec/models/case_court_report_spec.rb
+++ b/spec/models/case_court_report_spec.rb
@@ -104,6 +104,48 @@
expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A2, Type B2 \(12\/02\/20\): No Answer Provided.*/)
end
+ it "omits guidance and unanswered topics from real report templates" do
+ casa_case = create(:casa_case)
+ answered_topic = create(
+ :contact_topic,
+ casa_org: casa_case.casa_org,
+ question: "Included topic heading",
+ details: "Included topic guidance"
+ )
+ create(
+ :contact_topic,
+ casa_org: casa_case.casa_org,
+ question: "Omitted topic heading",
+ details: "Omitted topic guidance"
+ )
+ case_contact = create(:case_contact, casa_case: casa_case)
+ create(
+ :contact_topic_answer,
+ case_contact: case_contact,
+ contact_topic: answered_topic,
+ value: "Included topic answer"
+ )
+ template_paths = %w[
+ default_report_template.docx
+ montgomery_report_template.docx
+ prince_george_report_template.docx
+ ].map { |filename| Rails.root.join("app/documents/templates", filename).to_s }
+
+ template_paths.each do |template_path|
+ topics = CaseCourtReportContext.new(case_id: casa_case.id, path_to_template: template_path).court_topics.values
+ docx_response = generate_doc(full_context.merge(case_topics: topics), template_path)
+ document_text = (docx_response.paragraphs.map(&:to_s) + table_text(docx_response)).join(" ")
+
+ aggregate_failures(File.basename(template_path)) do
+ expect(document_text).to include("Included topic heading")
+ expect(document_text).to include("Included topic answer")
+ expect(document_text).not_to include("Included topic guidance")
+ expect(document_text).not_to include("Omitted topic heading")
+ expect(document_text).not_to include("Omitted topic guidance")
+ end
+ end
+ end
+
context "when there are topics but no answers" do
let(:curr_context) do
full_context[:case_topics] = [
diff --git a/spec/requests/casa_org_spec.rb b/spec/requests/casa_org_spec.rb
index 50500e19e7..a397bdc908 100644
--- a/spec/requests/casa_org_spec.rb
+++ b/spec/requests/casa_org_spec.rb
@@ -17,6 +17,15 @@
end
it { is_expected.to be_successful }
+
+ it "orders contact topics alphabetically" do
+ create(:contact_topic, casa_org: casa_org, question: "Zulu")
+ create(:contact_topic, casa_org: casa_org, question: "Alpha")
+
+ request
+
+ expect(assigns(:contact_topics).pluck(:question)).to eq(["Alpha", "Zulu"])
+ end
end
describe "PATCH /update" do
diff --git a/spec/requests/case_court_reports_spec.rb b/spec/requests/case_court_reports_spec.rb
index e3fcdc6c44..9ac2ee7273 100644
--- a/spec/requests/case_court_reports_spec.rb
+++ b/spec/requests/case_court_reports_spec.rb
@@ -120,6 +120,19 @@
request
end
+ it "passes the empty-topic option to the report context" do
+ params[:case_court_report][:include_empty_topics] = "1"
+ context_builder = instance_double(CaseCourtReportContext, context: {})
+ report = instance_double(CaseCourtReport, generate_to_string: "report")
+ allow(CaseCourtReport).to receive(:new).and_return(report)
+ allow_any_instance_of(CaseCourtReportsController).to receive(:save_report)
+ expect(CaseCourtReportContext).to receive(:new)
+ .with(hash_including(include_empty_topics: "1"))
+ .and_return(context_builder)
+
+ expect(request).to have_http_status(:ok)
+ end
+
context "when no custom template is set" do
it "sends response as a JSON string", :aggregate_failures do
expect(request.content_type).to eq("application/json; charset=utf-8")
diff --git a/spec/services/case_contacts_contact_dates_spec.rb b/spec/services/case_contacts_contact_dates_spec.rb
index 2b970667dc..ba29e7e0b4 100644
--- a/spec/services/case_contacts_contact_dates_spec.rb
+++ b/spec/services/case_contacts_contact_dates_spec.rb
@@ -17,9 +17,12 @@
end
context "with interviewees" do
- let(:contact_type_1) { create(:contact_type, name: "Mental therapist") }
- let(:contact_type_2) { create(:contact_type, name: "Physical therapist") }
- let(:contact_type_3) { create(:contact_type, name: "Aunt") }
+ let(:casa_org) { create(:casa_org) }
+ let(:family_group) { create(:contact_type_group, casa_org: casa_org, name: "Family") }
+ let(:health_group) { create(:contact_type_group, casa_org: casa_org, name: "Health") }
+ let(:contact_type_1) { create(:contact_type, contact_type_group: health_group, name: "Mental therapist") }
+ let(:contact_type_2) { create(:contact_type, contact_type_group: health_group, name: "Physical therapist") }
+ let(:contact_type_3) { create(:contact_type, contact_type_group: family_group, name: "Aunt") }
let(:ccct_1) { create(:case_contact_contact_type, contact_type: contact_type_1) }
let(:ccct_2) { create(:case_contact_contact_type, contact_type: contact_type_2) }
@@ -41,6 +44,10 @@
it "returns formatted data" do
expect(subject).to eq([
+ {dates: "4/01*",
+ dates_by_medium_type: {"in-person" => "4/01*"},
+ name: "Names of persons involved, starting with the child's name",
+ type: "Aunt"},
{dates: "6/01*",
dates_by_medium_type: {"in-person" => "6/01*"},
name: "Names of persons involved, starting with the child's name",
@@ -48,11 +55,7 @@
{dates: "4/01*, 5/01*, 6/01*",
dates_by_medium_type: {"in-person" => "5/01*, 6/01*", "text/email" => "4/01*"},
name: "Names of persons involved, starting with the child's name",
- type: "Physical therapist"},
- {dates: "4/01*",
- dates_by_medium_type: {"in-person" => "4/01*"},
- name: "Names of persons involved, starting with the child's name",
- type: "Aunt"}
+ type: "Physical therapist"}
])
end
end
diff --git a/spec/system/casa_cases/show_spec.rb b/spec/system/casa_cases/show_spec.rb
index 8d8a50fb9d..7927aafddb 100644
--- a/spec/system/casa_cases/show_spec.rb
+++ b/spec/system/casa_cases/show_spec.rb
@@ -61,6 +61,7 @@
within("#generate-court-report") do
expect(page).to have_content(casa_case.case_number)
+ expect(page).to have_unchecked_field("Include sections with no entries")
# No past hearing, so the window starts the day the case was opened in CASA -- not "today",
# which would be an empty window. (The case is created before this example freezes the clock.)
expect(page.find("#start_date").value).to eq(casa_case.created_at.to_date.to_s)
diff --git a/spec/system/case_court_reports/index_spec.rb b/spec/system/case_court_reports/index_spec.rb
index e24e6bb086..40fe34a4cc 100644
--- a/spec/system/case_court_reports/index_spec.rb
+++ b/spec/system/case_court_reports/index_spec.rb
@@ -62,6 +62,7 @@
it "shows the Generate button and a searchable picker", :aggregate_failures do
expect(page).to have_selector "#btnGenerateReport", text: "Generate report", visible: :visible
expect(page).to have_css "#generate-docx-report-modal .ts-wrapper"
+ expect(page).to have_unchecked_field("Include sections with no entries")
end
it "shows both dates empty until a case is chosen", :aggregate_failures do