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