diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6d94044bb1..30e926629f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,13 +16,13 @@ jobs:
strategy:
fail-fast: false
matrix:
- groups: ["[0, 1, 2, 3]", "[4, 5, 6, 7]", "[8, 9, 10, 11]"]
+ groups: ["[0, 1, 2]", "[3, 4, 5]", "[6, 7, 8]", "[9, 10, 11]"]
uses: ./.github/workflows/rspec.yml
secrets: inherit
with:
groups: ${{ matrix.groups }}
group_count: 12 # the total number of test groups, must match the groups listed in the matrix.groups
- parallel_processes_count: 4 # the number of parallel processes to run tests in worker, must match the size of the
+ parallel_processes_count: 3 # the number of parallel processes to run tests in worker, must match the size of the
# inner arrays in the matrix.groups
combine_and_report:
uses: ./.github/workflows/combine_and_report.yml
diff --git a/.github/workflows/combine_and_report.yml b/.github/workflows/combine_and_report.yml
index a9bf5f9c47..362a7085c8 100644
--- a/.github/workflows/combine_and_report.yml
+++ b/.github/workflows/combine_and_report.yml
@@ -15,15 +15,15 @@ jobs:
find artifacts -name "test_reports*.zip" -exec unzip -d test_reports {} \;
find test_reports -name "**/test_reports*.zip" -exec unzip -d test_reports {} \;
- name: Merge parallel runtime log parts
- if: github.repository == github.event.pull_request.head.repo.full_name
+ if: env.AZURE_STORAGE_KEY != ''
run: |
cat artifacts/**/parallel_runtime_rspec*.log > parallel_runtime.log
- name: Upload log file to Azure Blob Storage
- if: github.repository == github.event.pull_request.head.repo.full_name
env:
AZURE_STORAGE_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
AZURE_STORAGE_ACCOUNT: ${{ secrets.ACCOUNT_NAME }}
STORAGE_CONTAINER: ${{ secrets.STORAGE_CONTAINER }}
+ if: env.AZURE_STORAGE_KEY != ''
run: |
az storage blob upload \
-c $STORAGE_CONTAINER \
diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml
index e09da97923..106d618027 100644
--- a/.github/workflows/rspec.yml
+++ b/.github/workflows/rspec.yml
@@ -42,14 +42,12 @@ jobs:
- uses: actions/checkout@v4
- name: Download parallel runtime log from Azure Blob Storage
- if: github.repository == github.event.pull_request.head.repo.full_name
env:
AZURE_STORAGE_KEY: ${{ secrets.STORAGE_ACCESS_KEY }}
AZURE_STORAGE_ACCOUNT: ${{ secrets.ACCOUNT_NAME }}
STORAGE_CONTAINER: ${{ secrets.STORAGE_CONTAINER }}
+ if: env.AZURE_STORAGE_KEY != ''
run: |
- echo $STORAGE_CONTAINER
- echo ${{ secrets.STORAGE_CONTAINER }}
az storage blob download \
-c $STORAGE_CONTAINER \
--file old_parallel_runtime.log \
@@ -116,6 +114,7 @@ jobs:
--runtime-log old_parallel_runtime.log \
--verbose-command ./spec
+ echo 'Tests completed. Uploading to Code Climate'
./cc-test-reporter after-build --exit-code $?
cat tmp/spec_summary.log
@@ -124,7 +123,7 @@ jobs:
zip -r test_reports_${{ env.GROUPS_UNDERSCORE }}.zip tmp/reports
- name: Compress log
- if: github.repository == github.event.pull_request.head.repo.full_name
+ if: env.AZURE_STORAGE_KEY != ''
run: |
mv tmp/parallel_runtime.log parallel_runtime_rspec_${{ env.GROUPS_UNDERSCORE }}.log
@@ -135,7 +134,7 @@ jobs:
path: test_reports_${{ env.GROUPS_UNDERSCORE }}.zip
- name: Upload file parallel tests runtime log
- if: github.repository == github.event.pull_request.head.repo.full_name
+ if: env.AZURE_STORAGE_KEY != ''
uses: actions/upload-artifact@v4
with:
name: parallel_runtime_rspec_${{ env.GROUPS_UNDERSCORE }}.log
diff --git a/app/documents/templates/default_report_template.docx b/app/documents/templates/default_report_template.docx
old mode 100755
new mode 100644
index 6cdb2dfeaf..3c22af397f
Binary files a/app/documents/templates/default_report_template.docx and b/app/documents/templates/default_report_template.docx differ
diff --git a/app/documents/templates/howard_county_report_template.docx b/app/documents/templates/howard_county_report_template.docx
index 03c2e2e024..ac022eb596 100644
Binary files a/app/documents/templates/howard_county_report_template.docx and b/app/documents/templates/howard_county_report_template.docx differ
diff --git a/app/documents/templates/montgomery_report_template.docx b/app/documents/templates/montgomery_report_template.docx
old mode 100755
new mode 100644
index 52746fe328..deb07c1571
Binary files a/app/documents/templates/montgomery_report_template.docx and b/app/documents/templates/montgomery_report_template.docx differ
diff --git a/app/documents/templates/prince_george_report_template.docx b/app/documents/templates/prince_george_report_template.docx
old mode 100755
new mode 100644
index b74deb58a1..d8c4a24f2b
Binary files a/app/documents/templates/prince_george_report_template.docx and b/app/documents/templates/prince_george_report_template.docx differ
diff --git a/app/models/case_court_report_context.rb b/app/models/case_court_report_context.rb
index 122ccabb23..d91735d94a 100644
--- a/app/models/case_court_report_context.rb
+++ b/app/models/case_court_report_context.rb
@@ -25,7 +25,8 @@ def context
latest_hearing_date: latest_hearing_date,
org_address: org_address(@path_to_template),
volunteer: volunteer_info,
- hearing_type_name: @court_date&.hearing_type&.name || "None"
+ hearing_type_name: @court_date&.hearing_type&.name || "None",
+ case_topics: court_topics.values
}
end
@@ -88,6 +89,51 @@ def org_address(path_to_template)
@volunteer.casa_org.address if @volunteer && is_default_template
end
+ # Sample output
+ #
+ # expected_topics = {
+ # "Question 1" => {topic: "Question 1", details: "Details 1", 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: [
+ # {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: [
+ # {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"}
+ # ]}
+ # }
+ def court_topics
+ topics = ContactTopic
+ .joins(contact_topic_answers: {case_contact: [:casa_case, :contact_types]}).distinct
+ .where("casa_cases.id": @casa_case.id)
+ .where("case_contacts.occurred_at": @date_range)
+ .order(:occurred_at, :value)
+ .select(:details, :question, :occurred_at, :value, :contact_made,
+ "STRING_AGG(contact_types.name, ', ' ORDER BY contact_types.name) AS medium_types")
+ .group(:details, :question, :occurred_at, :value, :contact_made)
+
+ topics.each_with_object({}) do |topic, hash|
+ hash[topic.question] ||= {
+ answers: [],
+ topic: topic.question,
+ details: topic.details
+ }
+
+ formatted_date = CourtReportFormatContactDate.new(topic).format_long
+ answer_value = topic.value.blank? ? "No Answer Provided" : topic.value
+ answer = {
+ date: formatted_date,
+ medium: topic.medium_types,
+ value: answer_value
+ }
+
+ hash[topic.question][:answers].push(answer)
+ end
+ end
+
private
def calculate_date_range(args)
diff --git a/app/services/court_report_format_contact_date.rb b/app/services/court_report_format_contact_date.rb
index 6376ba29fd..db20bfbd66 100644
--- a/app/services/court_report_format_contact_date.rb
+++ b/app/services/court_report_format_contact_date.rb
@@ -8,4 +8,8 @@ def initialize(case_contact)
def format
I18n.l(@case_contact.occurred_at, format: :short_date, default: nil).concat(@case_contact.contact_made ? "" : CONTACT_UNSUCCESSFUL_PREFIX)
end
+
+ def format_long
+ I18n.l(@case_contact.occurred_at, format: :long_date, default: nil)
+ end
end
diff --git a/app/views/case_contacts/form/details.html.erb b/app/views/case_contacts/form/details.html.erb
index b2d869c748..92a248c560 100644
--- a/app/views/case_contacts/form/details.html.erb
+++ b/app/views/case_contacts/form/details.html.erb
@@ -79,6 +79,7 @@
+ <% if @case_contact.contact_topic_answers.any? %>
Court report <%= "topic".pluralize(@case_contact.contact_topic_answers.count) %> (optional)
@@ -90,6 +91,7 @@
<% end %>
+ <% end %>
<%= link_to leave_case_contacts_form_path, class: "btn-sm main-btn #{@case_contact.draft_case_ids.empty? ? 'danger' : 'primary'}-btn-outline btn-hover", data: { controller: "alert", "action": "alert#confirm", "alert-ignore-value": !@case_contact.draft_case_ids.empty?, "alert-title-value": "Discard draft?", "alert-message-value": "Are you sure? If you don't save and continue to the next step, this draft will not be recoverable." } do %>
Back
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3074d0e6a2..569a2af586 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -38,6 +38,7 @@ en:
full: "%B %-d, %Y"
youth_date_of_birth: "%B %Y"
short_date: "%-m/%d"
+ long_date: "%m/%d/%y"
edit_profile: "%B %d, %Y at %I:%M %p %Z"
time_on_date: "%-I:%-M %p on %m-%e-%Y"
date:
diff --git a/lib/tasks/deployment/20240420230126_update_org_templates.rake b/lib/tasks/deployment/20240420230126_update_org_templates.rake
new file mode 100644
index 0000000000..94b30c39ee
--- /dev/null
+++ b/lib/tasks/deployment/20240420230126_update_org_templates.rake
@@ -0,0 +1,29 @@
+namespace :after_party do
+ desc "Deployment task: Updates_production_casa_orgs_with_new_templates"
+ task update_org_templates: :environment do
+ puts "Running deploy task 'update_org_templates'"
+
+ mapping = {
+ "Howard County CASA" => "howard_county_report_template.docx",
+ "Voices for Children Montgomery" => "montgomery_report_template.docx",
+ "Prince George CASA" => "prince_george_report_template.docx"
+ }
+
+ mapping.each do |casa_org_name, template_file_name|
+ casa_org = CasaOrg.find_by(name: casa_org_name)
+ if casa_org
+ casa_org.court_report_template.attach(
+ io: File.new(Rails.root.join("app", "documents", "templates", template_file_name)),
+ filename: template_file_name
+ )
+ else
+ Bugsnag.notify("No #{casa_org_name} found for rake task update_org_templates")
+ end
+ end
+
+ # Update task as completed. If you remove the line below, the task will
+ # run with every deploy (or every time you call after_party:run).
+ AfterParty::TaskRecord
+ .create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
+ end
+end
diff --git a/spec/models/case_court_report_context_spec.rb b/spec/models/case_court_report_context_spec.rb
index 45ee767d1f..043191517b 100644
--- a/spec/models/case_court_report_context_spec.rb
+++ b/spec/models/case_court_report_context_spec.rb
@@ -26,6 +26,7 @@
allow(context).to receive(:org_address).and_return(nil)
allow(context).to receive(:volunteer_info).and_return({})
allow(context).to receive(:latest_hearing_date).and_return("")
+ allow(context).to receive(:court_topics).and_return({})
expected_shape = {
created_date: "January 1, 2021",
@@ -36,7 +37,8 @@
latest_hearing_date: "",
org_address: nil,
volunteer: {},
- hearing_type_name: court_date.hearing_type.name
+ hearing_type_name: court_date.hearing_type.name,
+ case_topics: []
}
expect(context.context).to eq(expected_shape)
@@ -176,6 +178,82 @@
end
end
+ describe "#court_topics" do
+ let(:org) { create(:casa_org) }
+ let(:casa_case) { create(:casa_case, casa_org: org) }
+ let(:topics) { [1, 2, 3].map { |i| create(:contact_topic, casa_org: org, question: "Question #{i}", details: "Details #{i}") } }
+ let(:contacts) do
+ [1, 2, 3, 4].map do |i|
+ create(:case_contact,
+ casa_case: casa_case,
+ occurred_at: 1.month.ago + i.days,
+ contact_types: [
+ create(:contact_type, name: "Type A#{i}"),
+ create(:contact_type, name: "Type B#{i}")
+ ])
+ end
+ end
+ # let(:contacts) { create_list(:case_contact, 4, casa_case: casa_case, occurred_at: 1.month.ago) }
+
+ context "when given data" do
+ # Add some values that should get filtered out
+ before do
+ contact_one = create(:case_contact, casa_case: casa_case, medium_type: "in-person", occurred_at: 1.day.ago)
+ create_list(:contact_topic_answer, 2, case_contact: contact_one, contact_topic: topics[0], value: "Not included")
+
+ contact_two = create(:case_contact, casa_case: casa_case, medium_type: "in-person", occurred_at: 50.day.ago)
+ create_list(:contact_topic_answer, 2, case_contact: contact_two, contact_topic: topics[0], value: "Not included")
+
+ other_case = create(:casa_case, casa_org: org)
+ contact_three = create(:case_contact, casa_case: other_case, medium_type: "in-person", occurred_at: 50.day.ago)
+ create_list(:contact_topic_answer, 2, case_contact: contact_three, contact_topic: topics[0], value: "Not included")
+ end
+
+ it "generates correctly shaped data" do
+ # Contact 1 Answers
+ create(:contact_topic_answer, case_contact: contacts[0], contact_topic: topics[0], value: "Answer 1")
+ create(:contact_topic_answer, case_contact: contacts[0], contact_topic: topics[1], value: "Answer 2")
+
+ # Contact 2 Answers
+ create(:contact_topic_answer, case_contact: contacts[1], contact_topic: topics[0], value: "Answer 3")
+ create(:contact_topic_answer, case_contact: contacts[1], contact_topic: topics[2], value: nil)
+
+ # Contact 3 Answers
+ create(:contact_topic_answer, case_contact: contacts[2], contact_topic: topics[1], value: "Answer 5")
+ create(:contact_topic_answer, case_contact: contacts[2], contact_topic: topics[2], value: "")
+
+ # Contact 4 Answers
+ # No Answers
+
+ expected_topics = {
+ "Question 1" => {topic: "Question 1", details: "Details 1", 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: [
+ {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: [
+ {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"}
+ ]}
+ }
+
+ court_report_context = build(:case_court_report_context, start_date: 45.day.ago.to_s, end_date: 5.day.ago.to_s, casa_case: casa_case)
+
+ expect(court_report_context.court_topics).to eq(expected_topics)
+ end
+ end
+
+ context "when there are no contact topics" do
+ it "returns an empty hash" do
+ court_report_context = build(:case_court_report_context, start_date: 45.day.ago.to_s, end_date: 5.day.ago.to_s, casa_case: casa_case)
+ expect(court_report_context.court_topics).to eq({})
+ end
+ end
+ end
+
describe "#filtered_interviewees" do
it "filters based on date range" do
casa_case = create(:casa_case)
diff --git a/spec/models/case_court_report_spec.rb b/spec/models/case_court_report_spec.rb
index 17783455dd..afbd6234d2 100644
--- a/spec/models/case_court_report_spec.rb
+++ b/spec/models/case_court_report_spec.rb
@@ -9,6 +9,114 @@
let(:path_to_report) { Rails.root.join("tmp", "test_report.docx").to_s }
context "#generate_to_string" do
+ let(:full_context) do
+ {
+ created_date: "April 9, 2024",
+ casa_case: {
+ court_date: "April 23, 2024",
+ case_number: "A-CASA-CASE-NUMBER-12345",
+ dob: "April 2012",
+ is_transitioning: false,
+ judge_name: "Judge Judy"
+ },
+ case_contacts: [
+ {name: "Some Name", type: "Type 1", dates: "4/09*", dates_by_medium_type: {"in-person" => "4/09*"}},
+ {name: "Some Other Name", type: "Type 4", dates: "4/09*", dates_by_medium_type: {"in-person" => "4/09*"}}
+ ],
+ case_court_orders: [
+ {order: "case_court_order_text", status: "Partially implemented"}
+ ],
+ case_mandates: [
+ {order: "case_mandates_text", status: "Partially implemented"}
+ ],
+ latest_hearing_date: "_______",
+ org_address: "596 Unique Avenue Seattle, Washington",
+ volunteer: {
+ name: "name_of_volunteer",
+ supervisor_name: "name_of_supervisor",
+ assignment_date: "February 9, 2024"
+ },
+ hearing_type_name: "None",
+ case_topics: [
+ {topic: "Question 1", details: "Details 1", answers: [
+ {date: "12/01/20", medium: "Type A1, Type B1", value: "Answer 1"},
+ {date: "12/02/20", medium: "Type A2, Type B2", value: "Answer 3"}
+ ]},
+ {topic: "Question 2", details: "Details 2", answers: [
+ {date: "12/01/20", medium: "Type A1, Type B1", value: "Answer 2"},
+ {date: "12/02/20", medium: "Type A3, Type B3", value: "Answer 5"}
+ ]},
+ {topic: "Question 3", details: "Details 3", answers: [
+ {date: "12/01/20", medium: "Type A3, Type B3", value: "No Answer Provided"},
+ {date: "12/02/20", medium: "Type A2, Type B2", value: "No Answer Provided"}
+ ]}
+ ]
+ }
+ end
+ describe "contact_topics" do
+ it "all contact topics are present in the report" do
+ docx_response = generate_doc(full_context, path_to_template)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 1.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 2.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 3.*/)
+ end
+
+ it "all topic details are present in the report" do
+ docx_response = generate_doc(full_context, path_to_template)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 1.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 2.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 3.*/)
+ end
+
+ it "all answers are present with correct format" do
+ docx_response = generate_doc(full_context, path_to_template)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A1, Type B1 \(12\/01\/20\): Answer 1.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A2, Type B2 \(12\/02\/20\): Answer 3.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A1, Type B1 \(12\/01\/20\): Answer 2.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A3, Type B3 \(12\/02\/20\): Answer 5.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A3, Type B3 \(12\/01\/20\): No Answer Provided.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Type A2, Type B2 \(12\/02\/20\): No Answer Provided.*/)
+ end
+
+ context "when there are topics but no answers" do
+ let(:curr_context) do
+ full_context[:case_topics] = [
+ {topic: "Question 1", details: "Details 1", answers: []},
+ {topic: "Question 2", details: "Details 2", answers: []},
+ {topic: "Question 3", details: "Details 3", answers: []}
+ ]
+ end
+
+ it "all contact topics are present in the report" do
+ docx_response = generate_doc(full_context, path_to_template)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 1.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 2.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Question 3.*/)
+ end
+ it "all topic details are present in the report" do
+ docx_response = generate_doc(full_context, path_to_template)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 1.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 2.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Details 3.*/)
+ end
+ end
+
+ context "when there no topics" do
+ it "report does not error and puts old defaults" do
+ full_context[:case_topics] = []
+ docx_response = nil
+ expect {
+ docx_response = generate_doc(full_context, path_to_template)
+ }.not_to raise_error
+
+ expect(docx_response).not_to be_nil
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Placement.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Education\/Vocation.*/)
+ expect(docx_response.paragraphs.map(&:to_s)).to include(/Objective Information.*/)
+ end
+ end
+ end
+
describe "when receiving valid case, volunteer, and path_to_template" do
let(:volunteer) { create(:volunteer, :with_cases_and_contacts, :with_assigned_supervisor) }
let(:casa_case_with_contacts) { volunteer.casa_cases.first }
@@ -367,3 +475,8 @@
end
end
end
+
+def generate_doc(context, path_to_template)
+ report = CaseCourtReport.new(path_to_template: path_to_template, context: context)
+ Docx::Document.open(StringIO.new(report.generate_to_string))
+end
diff --git a/spec/requests/case_contacts/form_spec.rb b/spec/requests/case_contacts/form_spec.rb
index afb89b961b..2ba0070bb6 100644
--- a/spec/requests/case_contacts/form_spec.rb
+++ b/spec/requests/case_contacts/form_spec.rb
@@ -58,6 +58,15 @@
end
end
+ context "when an org has no topics" do
+ let(:organization) { create(:casa_org) }
+ let!(:case_contact) { create(:case_contact, :details_status, casa_case: casa_case) }
+
+ it "does not show contact topic card" do
+ page = request.parsed_body.to_html
+ expect(page).to_not include("Court report topics")
+ end
+ end
context "when the org has topics assigned" do
let(:contact_topics) {
[
diff --git a/spec/system/case_contacts/additional_expenses_spec.rb b/spec/system/case_contacts/additional_expenses_spec.rb
index 6f0781de6d..b5ff7e104c 100644
--- a/spec/system/case_contacts/additional_expenses_spec.rb
+++ b/spec/system/case_contacts/additional_expenses_spec.rb
@@ -197,6 +197,7 @@
expect(page).to have_no_field("case_contact_additional_expenses_attributes_10_other_expense_amount")
expect(page).to have_no_field("case_contact_additional_expenses_attributes_10_other_expenses_describe")
+ expect(casa_case.case_contacts.last.additional_expenses.count).to eq(10)
expect(page).to have_no_text("Add Another Expense")
end