From b3e1a4e4c0523c2e5ae7fa3377c0e07ee35cd26f Mon Sep 17 00:00:00 2001 From: Dave G <149399758+dtgreiner@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:22:23 -0500 Subject: [PATCH 01/10] max threshold for source clients (#5101) * add max number or source clients * comments & cleanup * additional test cleanup & comments * Update spec/models/grda_warehouse/tasks/identify_duplicates_spec.rb Co-authored-by: Elliot * clear out old artifacts * fix failing tests --------- Co-authored-by: Elliot --- .../tasks/identify_duplicates.rb | 18 +++++- .../tasks/identify_duplicates_spec.rb | 61 +++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/app/models/grda_warehouse/tasks/identify_duplicates.rb b/app/models/grda_warehouse/tasks/identify_duplicates.rb index efad9f58408..b4e888601b4 100644 --- a/app/models/grda_warehouse/tasks/identify_duplicates.rb +++ b/app/models/grda_warehouse/tasks/identify_duplicates.rb @@ -9,6 +9,8 @@ class IdentifyDuplicates include ArelHelper include NotifierConfig + MAX_SOURCE_CLIENTS = 50 + def initialize(run_post_processing: true) setup_notifier('IdentifyDuplicates') @run_post_processing = run_post_processing @@ -144,8 +146,10 @@ def match_existing! # This has already been fully merged next if source_id == destination_id - destination = client_destinations.find(destination_id) - source = client_destinations.find(source_id) + destination = client_destinations.find_by(id: destination_id) + source = client_destinations.find_by(id: source_id) + next unless destination.present? && source.present? + begin destination.merge_from(source, reviewed_by: user, reviewed_at: DateTime.current) rescue Exception => e @@ -241,9 +245,17 @@ def match_existing! ) end + # Destination clients are limited to MAX_SOURCE_CLIENTS number of source clients. This is to prevent runaway duplicate merges. + # This will return the ids of any destination clients that is at or beyond this threshold so they can be filtered out of future matching. + # We are using unmatchable destinations here to more easily include destination clients who do not have a warehouse client record. + # This should also be a smaller list than the full list of matchable destination ids. + private def unmatchable_destination_ids + GrdaWarehouse::WarehouseClient.group(:destination_id).having('count(*) >= ?', MAX_SOURCE_CLIENTS).select(:destination_id) + end + # fetch a list of existing clients from the DND Warehouse DataSource (current destinations) private def client_destinations - GrdaWarehouse::Hud::Client.destination + GrdaWarehouse::Hud::Client.destination.where.not(id: unmatchable_destination_ids) end # Look for really clear matches (2 of the following 3 should be good): diff --git a/spec/models/grda_warehouse/tasks/identify_duplicates_spec.rb b/spec/models/grda_warehouse/tasks/identify_duplicates_spec.rb index d93551d7cd7..f27ea1da58d 100644 --- a/spec/models/grda_warehouse/tasks/identify_duplicates_spec.rb +++ b/spec/models/grda_warehouse/tasks/identify_duplicates_spec.rb @@ -127,6 +127,67 @@ end end end + + describe 'source client threshold' do + before(:each) do + # We want to know the PPI data for this client, so set it specifically + client_in_source.update(first_name: 'A', last_name: 'Client', dob: '2000-01-01', ssn: 'XXXXX1234') + # Clear out the existing destination client, this will be generated for `client_in_source` later in the test + client_in_destination.destroy + end + it 'never creates more than the maximum number of source clients' do + number_sample_clients = 125 + + # Generating n unique clients that won't match the existing client or each other. + # We are generating `number_sample_clients` - 1 because we are using the existing client (`client_in_source`) + # as a baseline. This will bring our total number of clients to `number_sample_clients`. + (1..(number_sample_clients - 1)).each do |n| + client = create :grda_warehouse_hud_client, data_source: source_data_source + date = Date.new(2000, 1, 1) + n.days - n.months + str_n = n.to_s.rjust(4, '0') + ssn = "#{str_n.last(3)}#{str_n.last(2)}#{str_n.last(4)}" + client.update(first_name: "client_#{n}", last_name: 'Test', dob: date, ssn: ssn) + end + + GrdaWarehouse::Tasks::IdentifyDuplicates.new.run! + GrdaWarehouse::Tasks::IdentifyDuplicates.new.match_existing! + + # Test that each unique client received a destination client + expect(GrdaWarehouse::Hud::Client.destination.count).to eq(number_sample_clients) + expect(GrdaWarehouse::WarehouseClient.count).to eq(number_sample_clients) + + # Update the n unique clients so that they all match the baseline client. + (1..(number_sample_clients - 1)).each do |n| + str_n = n.to_s.rjust(4, '0') + ssn = "#{str_n.last(3)}#{str_n.last(2)}#{str_n.last(4)}" + client = GrdaWarehouse::Hud::Client.source.find_by(SSN: ssn) + client.update(first_name: 'A', last_name: 'Client', dob: '2000-01-01', ssn: 'XXXXX1234') + end + + GrdaWarehouse::Tasks::IdentifyDuplicates.new.run! + GrdaWarehouse::Tasks::IdentifyDuplicates.new.match_existing! + + # # The code below will calculate the number of expected destination clients in the case that MAX_SOURCE_CLIENTS changes enought to affect this number + # expected_number_destination_clients = ((number_sample_clients * 1.0) / GrdaWarehouse::Tasks::IdentifyDuplicates::MAX_SOURCE_CLIENTS).ceil + + # With MAX_SOURCE_CLIENTS set to 50, we are expecting 3 destiantion clients. 2 with 50 source clients and 1 with 25 source clients. + # We are setting this specifically instead of using the calculated number in case `MAX_SOURCE_CLIENTS` gets set to a number larger than + # `number_sample_clients`. If that happened, we wouldn't be reaching the threshold for the numebr of source clients that we are testing. + expected_number_destination_clients = 3 + + destination_clients = GrdaWarehouse::Hud::Client.destination.to_a + + expect(destination_clients.count).to eq(expected_number_destination_clients) + expect(GrdaWarehouse::WarehouseClient.count).to eq(number_sample_clients) + + # Pop the last client off of the array. This client will have less than the maximum number of source clients. + last_client = destination_clients.pop + destination_clients.each do |client| + expect(client.source_clients.count).to eq(GrdaWarehouse::Tasks::IdentifyDuplicates::MAX_SOURCE_CLIENTS) + end + expect(last_client.source_clients.count).to eq(number_sample_clients % GrdaWarehouse::Tasks::IdentifyDuplicates::MAX_SOURCE_CLIENTS) + end + end end describe 'When matching is disabled' do From 816e6352db24bd46c3e3ddc323104cd7c000623e Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 3 Feb 2025 16:22:48 -0500 Subject: [PATCH 02/10] Redact names everywhere on the client dashboard and some other locations as well (#5099) --- .../clients/anomalies_controller.rb | 2 +- app/controllers/clients/audits_controller.rb | 2 +- .../clients/cas_readiness_controller.rb | 2 +- app/controllers/clients/chronic_controller.rb | 2 +- ...oordinated_entry_assessments_controller.rb | 2 +- .../clients/enrollment_history_controller.rb | 2 +- app/controllers/clients/files_controller.rb | 2 +- .../clients/hud_lots_controller.rb | 2 +- app/controllers/clients/notes_controller.rb | 2 +- .../clients/releases_controller.rb | 4 +-- app/controllers/clients/users_controller.rb | 2 +- .../clients/vispdats_controller.rb | 2 +- .../clients/youth/intakes_controller.rb | 2 +- .../cohorts/client_notes_controller.rb | 2 +- app/controllers/cohorts/clients_controller.rb | 2 +- app/controllers/cohorts/notes_controller.rb | 2 +- app/controllers/concerns/activity_logger.rb | 2 +- app/models/grda_warehouse/pii_provider.rb | 32 +++++++++++++---- app/views/clients/_assessment_form.html.haml | 10 ++---- app/views/clients/_enrollment_table.haml | 5 ++- app/views/clients/_match_results.html.haml | 27 ++++---------- app/views/clients/_new_client_form.haml | 9 +++-- .../clients/_potential_matches.html.haml | 9 +++-- app/views/clients/_unmerge_form.haml | 11 +++--- app/views/clients/anomalies/_breadcrumbs.haml | 4 +-- .../cas_readiness/_active_clients.haml | 2 +- .../cas_readiness/_ce_with_assessment.haml | 2 +- app/views/clients/cas_readiness/_chronic.haml | 2 +- .../clients/cas_readiness/_hud_chronic.haml | 2 +- .../clients/cas_readiness/_project_group.haml | 2 +- .../_release_and_project_group.haml | 2 +- .../cas_readiness/_release_present.haml | 2 +- .../coordinated_entry_assessments/edit.haml | 4 +-- .../coordinated_entry_assessments/new.haml | 4 +-- .../coordinated_entry_assessments/show.haml | 4 +-- app/views/clients/edit.haml | 2 +- .../clients/enrollment_history/index.haml | 2 +- app/views/clients/enrollments/show.haml | 2 +- .../clients/files/_permission_warning.haml | 4 +-- app/views/clients/notes/index.haml | 4 +-- app/views/clients/releases/_form.haml | 2 +- app/views/clients/rollup/_family.html.haml | 9 ++--- app/views/clients/simple.haml | 4 +-- app/views/clients/users/index.haml | 4 +-- app/views/clients/vispdats/_list.haml | 2 +- app/views/clients/vispdats/edit.haml | 2 +- app/views/clients/vispdats/new.haml | 2 +- app/views/clients/vispdats/show.haml | 4 +-- .../youth/housing_resolution_plans/_form.haml | 4 +-- .../clients/youth/intakes/_breadcrumbs.haml | 2 +- app/views/clients/youth/intakes/_intakes.haml | 4 +-- app/views/clients/youth/intakes/edit.haml | 4 +-- app/views/clients/youth/intakes/index.haml | 4 +-- app/views/clients/youth/intakes/new.haml | 4 +-- app/views/clients/youth/intakes/show.haml | 4 +-- app/views/he/clients/boston_covid_19.haml | 2 +- .../boston_covid_19/_isolation_form.haml | 4 +-- .../boston_covid_19/_quarantine_form.haml | 3 +- .../clients/boston_covid_19/_triage_form.haml | 6 ++-- .../clients/covid_19_vaccinations_only.haml | 2 +- app/views/projects/_clients.haml | 3 +- app/views/source_clients/edit.haml | 2 +- app/views/source_data/show.haml | 36 +++++++++++++++++-- .../warehouse_reports/anomalies/_section.haml | 2 +- .../cas/health_prioritization/index.haml | 2 +- .../health_prioritization/index.xlsx.axlsx | 2 +- .../ce_assessments/index.haml | 5 +-- .../conflicting_client_attributes/index.haml | 7 ++-- .../warehouse_reports/consent/index.haml | 2 +- .../dob_entry_same/index.haml | 5 +-- .../dv_victim_service/_table.haml | 2 +- .../expiring_consent/index.haml | 6 ++-- .../medical_restrictions/_table.haml | 7 ++-- .../inactive_youth_intakes/_table.haml | 2 +- .../warehouse_reports/incomes/index.haml | 2 +- .../warehouse_reports/outflow/details.haml | 2 +- .../warehouse_reports/recidivism/index.haml | 2 +- .../time_homeless_for_exits/_table.haml | 2 +- .../youth_activity/index.haml | 2 +- .../history_controller.rb | 2 +- .../clients/enrollment_details.haml | 2 +- .../client_access_control/clients/simple.haml | 2 +- .../warehouse_reports/reports/index.haml | 2 +- .../client_location_history/location.rb | 4 +-- .../client_location_history/clients/map.haml | 2 +- .../clients/rollup/_financial_clients.haml | 2 +- .../app/views/financial/clients/show.haml | 2 +- .../warehouse_reports/reports/_report.haml | 5 +-- .../warehouse_reports/reports/index.haml | 2 +- 89 files changed, 204 insertions(+), 169 deletions(-) diff --git a/app/controllers/clients/anomalies_controller.rb b/app/controllers/clients/anomalies_controller.rb index 5cc90b81823..0bd51f54329 100644 --- a/app/controllers/clients/anomalies_controller.rb +++ b/app/controllers/clients/anomalies_controller.rb @@ -66,7 +66,7 @@ def flash_interpolation_options end protected def title_for_show - "#{@client.name} - Anomalies" + "#{@client.pii_provider(user: current_user).full_name} - Anomalies" end private def anomaly_params diff --git a/app/controllers/clients/audits_controller.rb b/app/controllers/clients/audits_controller.rb index 0a6e8f92cd4..7ad3c48e3b4 100644 --- a/app/controllers/clients/audits_controller.rb +++ b/app/controllers/clients/audits_controller.rb @@ -28,6 +28,6 @@ def set_client end def title_for_show - "#{@client.name} - Audit" + "#{@client.pii_provider(user: current_user).full_name} - Audit" end end diff --git a/app/controllers/clients/cas_readiness_controller.rb b/app/controllers/clients/cas_readiness_controller.rb index b2f141bcd12..93e5cd59d09 100644 --- a/app/controllers/clients/cas_readiness_controller.rb +++ b/app/controllers/clients/cas_readiness_controller.rb @@ -65,7 +65,7 @@ def cas_readiness_params end def title_for_show - "#{@client.name} - CAS Readiness" + "#{@client.pii_provider(user: current_user).full_name} - CAS Readiness" end end end diff --git a/app/controllers/clients/chronic_controller.rb b/app/controllers/clients/chronic_controller.rb index 79961871d3e..62e2ee3366c 100644 --- a/app/controllers/clients/chronic_controller.rb +++ b/app/controllers/clients/chronic_controller.rb @@ -47,7 +47,7 @@ def cas_readiness_params end def title_for_show - "#{@client.name} - Chronic" + "#{@client.pii_provider(user: current_user).full_name} - Chronic" end end end diff --git a/app/controllers/clients/coordinated_entry_assessments_controller.rb b/app/controllers/clients/coordinated_entry_assessments_controller.rb index 62694b7aa72..4febe8878f4 100644 --- a/app/controllers/clients/coordinated_entry_assessments_controller.rb +++ b/app/controllers/clients/coordinated_entry_assessments_controller.rb @@ -96,7 +96,7 @@ def update end private def title_for_show - "#{@client.name} - #{Translation.translate('Coordinated Entry Assessment')}" + "#{@client.pii_provider(user: current_user).full_name} - #{Translation.translate('Coordinated Entry Assessment')}" end def flash_interpolation_options diff --git a/app/controllers/clients/enrollment_history_controller.rb b/app/controllers/clients/enrollment_history_controller.rb index 8eb8b2def6b..b4547be8e40 100644 --- a/app/controllers/clients/enrollment_history_controller.rb +++ b/app/controllers/clients/enrollment_history_controller.rb @@ -40,7 +40,7 @@ def history_scope end def title_for_show - "#{@client.name} - Historical Enrollments" + "#{@client.pii_provider(user: current_user).full_name} - Historical Enrollments" end end end diff --git a/app/controllers/clients/files_controller.rb b/app/controllers/clients/files_controller.rb index c27c25a79c8..228bed97cfe 100644 --- a/app/controllers/clients/files_controller.rb +++ b/app/controllers/clients/files_controller.rb @@ -268,7 +268,7 @@ def file_source end protected def title_for_show - "#{@client.name} - Files" + "#{@client.pii_provider(user: current_user).full_name} - Files" end def window_visible?(visibility) diff --git a/app/controllers/clients/hud_lots_controller.rb b/app/controllers/clients/hud_lots_controller.rb index d9ce2d66a2f..758d25f0995 100644 --- a/app/controllers/clients/hud_lots_controller.rb +++ b/app/controllers/clients/hud_lots_controller.rb @@ -29,7 +29,7 @@ def index end private def title_for_show - "#{@client.name} - Client-Level System Use & Length of Time Homeless Report" + "#{@client.pii_provider(user: current_user).full_name} - Client-Level System Use & Length of Time Homeless Report" end helper_method :title_for_show diff --git a/app/controllers/clients/notes_controller.rb b/app/controllers/clients/notes_controller.rb index 44e4fdf8ebd..268e10d71b6 100644 --- a/app/controllers/clients/notes_controller.rb +++ b/app/controllers/clients/notes_controller.rb @@ -112,7 +112,7 @@ def destroy end private def title_for_show - "#{@client.name} - Notes" + "#{@client.pii_provider(user: current_user).full_name} - Notes" end end end diff --git a/app/controllers/clients/releases_controller.rb b/app/controllers/clients/releases_controller.rb index 860936df37c..1a1ac6a68f6 100644 --- a/app/controllers/clients/releases_controller.rb +++ b/app/controllers/clients/releases_controller.rb @@ -139,7 +139,7 @@ def pre_populated private def render_pdf! @pdf = true - file_name = "Release of Information for #{@client.name}" + file_name = "Release of Information for #{@client.pii_provider(user: current_user).full_name}" send_data roi_pdf(file_name), filename: "#{file_name}.pdf", type: 'application/pdf' end @@ -175,7 +175,7 @@ def file_source end protected def title_for_show - "#{@client.name} - Release of Information" + "#{@client.pii_provider(user: current_user).full_name} - Release of Information" end def window_visible?(_visibility) diff --git a/app/controllers/clients/users_controller.rb b/app/controllers/clients/users_controller.rb index 2d5fa35ca29..ea0c6eabd1b 100644 --- a/app/controllers/clients/users_controller.rb +++ b/app/controllers/clients/users_controller.rb @@ -76,7 +76,7 @@ def set_user end protected def title_for_show - "#{@client.name} - Relationships" + "#{@client.pii_provider(user: current_user).full_name} - Relationships" end end end diff --git a/app/controllers/clients/vispdats_controller.rb b/app/controllers/clients/vispdats_controller.rb index f7a2da6202d..b5e5700ec79 100644 --- a/app/controllers/clients/vispdats_controller.rb +++ b/app/controllers/clients/vispdats_controller.rb @@ -164,7 +164,7 @@ def destroy_file end private def title_for_show - "#{@client.name} - VI-SPDATs" + "#{@client.pii_provider(user: current_user).full_name} - VI-SPDATs" end end end diff --git a/app/controllers/clients/youth/intakes_controller.rb b/app/controllers/clients/youth/intakes_controller.rb index 4fde5bad0d9..9c83cd5a173 100644 --- a/app/controllers/clients/youth/intakes_controller.rb +++ b/app/controllers/clients/youth/intakes_controller.rb @@ -88,7 +88,7 @@ def remove_all_youth_data @client.youth_follow_ups.destroy_all # TODO: This does not remove the client from the Youth DataSource - flash[:notice] = "All Youth information for #{@client.name} has been removed." + flash[:notice] = "All Youth information for #{@client.pii_provider(user: current_user).full_name} has been removed." redirect_to client_youth_intakes_path(@client) else not_authorized! diff --git a/app/controllers/cohorts/client_notes_controller.rb b/app/controllers/cohorts/client_notes_controller.rb index 399e7d4f71f..0256ffe589e 100644 --- a/app/controllers/cohorts/client_notes_controller.rb +++ b/app/controllers/cohorts/client_notes_controller.rb @@ -83,7 +83,7 @@ def cohort_id end def flash_interpolation_options - { resource_name: "Note for #{@note.client.name}" } + { resource_name: "Note for #{@note.client.pii_provider(user: current_user).full_name}" } end end end diff --git a/app/controllers/cohorts/clients_controller.rb b/app/controllers/cohorts/clients_controller.rb index 805a9a2c69a..6326340dabb 100644 --- a/app/controllers/cohorts/clients_controller.rb +++ b/app/controllers/cohorts/clients_controller.rb @@ -485,7 +485,7 @@ def destroy else log_removal(@client.cohort_id, @client.id, params.dig(:grda_warehouse_cohort_client, :reason)) if @client.destroy - flash[:notice] = "Removed #{@client.name}" + flash[:notice] = "Removed #{@client.pii_provider(user: current_user).full_name}" redirect_to cohort_path(@cohort) else render :pre_destroy diff --git a/app/controllers/cohorts/notes_controller.rb b/app/controllers/cohorts/notes_controller.rb index d6b7435830c..1cc2ce4568f 100644 --- a/app/controllers/cohorts/notes_controller.rb +++ b/app/controllers/cohorts/notes_controller.rb @@ -88,7 +88,7 @@ def cohort_id end def flash_interpolation_options - { resource_name: "Note for #{@note.client.name}" } + { resource_name: "Note for #{@note.client.pii_provider(user: current_user).full_name}" } end end end diff --git a/app/controllers/concerns/activity_logger.rb b/app/controllers/concerns/activity_logger.rb index c9d37060638..ee303088d50 100644 --- a/app/controllers/concerns/activity_logger.rb +++ b/app/controllers/concerns/activity_logger.rb @@ -58,7 +58,7 @@ def log_activity # override as necessary in the controller protected def title_for_show - return @client.name if @client.present? + return @client.pii_provider(user: current_user).full_name if @client.present? return @user.name if @user.present? end diff --git a/app/models/grda_warehouse/pii_provider.rb b/app/models/grda_warehouse/pii_provider.rb index adadaf420f2..b7e2e5b4c99 100644 --- a/app/models/grda_warehouse/pii_provider.rb +++ b/app/models/grda_warehouse/pii_provider.rb @@ -17,6 +17,12 @@ def self.viewable_name(value, policy:, replacement: REDACTED) value.presence end + def self.viewable_ssn(value, policy:, replacement: REDACTED) + return replacement unless policy.can_view_full_ssn? + + value.presence + end + def self.viewable_dob(value, policy:, replacement: REDACTED) return replacement unless policy.can_view_full_dob? @@ -46,32 +52,44 @@ def self.from_attributes(policy: nil, first_name: nil, last_name: nil, middle_na new(record, policy: policy) end + def redact_name? + ! policy.can_view_name? + end + + def redact_ssn? + ! policy.can_view_full_ssn? + end + + def redact_dob? + ! policy.can_view_full_dob? + end + def first_name - return name_redacted unless policy.can_view_name? + return name_redacted if redact_name? record.first_name.presence end def last_name - return name_redacted unless policy.can_view_name? + return name_redacted if redact_name? record.last_name.presence end def middle_name - return name_redacted unless policy.can_view_name? + return name_redacted if redact_name? record.middle_name.presence end def full_name - return name_redacted unless policy.can_view_name? + return name_redacted if redact_name? [record.first_name, record.middle_name, record.last_name].compact.join(' ').presence end def brief_name - return name_redacted unless policy.can_view_name? + return name_redacted if redact_name? [record.first_name, record.last_name].compact.join(' ').presence end @@ -84,7 +102,7 @@ def dob_and_age(force_year_only: false) return nil unless record.dob display_dob = record.dob - display_dob = display_dob&.year if force_year_only || !policy.can_view_full_dob? + display_dob = display_dob&.year if force_year_only || redact_dob? "#{display_dob} (#{age})" end @@ -101,7 +119,7 @@ def age def ssn(force_mask: false) value = record.ssn.presence - mask = force_mask || !policy.can_view_full_ssn? + mask = force_mask || redact_ssn? format_ssn(value, mask: mask) if value end diff --git a/app/views/clients/_assessment_form.html.haml b/app/views/clients/_assessment_form.html.haml index 7fd4b24d614..f07c207165d 100644 --- a/app/views/clients/_assessment_form.html.haml +++ b/app/views/clients/_assessment_form.html.haml @@ -1,13 +1,13 @@ - if @form[:answers].present? - form_name = (@form.assessment_type == @form.name) ? @form.name : "#{@form.assessment_type} < #{@form.name}" - content_for :modal_title, form_name - + .d-flex .w-100 .ssm__summary.d-flex.flex-column .mb-2 %dt.inline Name: - %dd.inline= @client.name + %dd.inline= @client.pii_provider(user: current_user).full_name .mb-2 %dt.inline Date Completed: %dd.inline= @form.collected_at&.to_date @@ -29,9 +29,3 @@ .client__assessment-answer= question[:answer] - else - content_for :modal_title, "Assessment Form Not Found" - - - - - - diff --git a/app/views/clients/_enrollment_table.haml b/app/views/clients/_enrollment_table.haml index 19fcf5d9bc9..755b0ace4d6 100644 --- a/app/views/clients/_enrollment_table.haml +++ b/app/views/clients/_enrollment_table.haml @@ -120,7 +120,10 @@ - tooltip += "Exit: #{c['last_date_in_program']}
" .mb-2.mt-2{ data: { toggle: :tooltip, title: tooltip, html: 'true', boundary: :window } } = link_to client_path(c['client_id']), class: 'd-block' do - #{c['FirstName']} #{c['LastName']} + - if @client.pii_provider(user: current_user).redact_name? + = @client.pii_provider(user: current_user).first_name + - else + #{c['FirstName']} #{c['LastName']} - if c['head_of_household'] %i.icon-user %br diff --git a/app/views/clients/_match_results.html.haml b/app/views/clients/_match_results.html.haml index ff17f23480a..fb00a25e002 100644 --- a/app/views/clients/_match_results.html.haml +++ b/app/views/clients/_match_results.html.haml @@ -5,12 +5,12 @@ %thead %tr %th{colspan: 2} Client - %th DOB + %th DOB/Age %th SSN %tbody - clients.each do |c| %tr.client__potential-match - - client_name = "#{c.full_name}" + - client_name = "#{c.pii_provider(user: current_user).full_name}" - sc_count = c.source_clients.count - colspan = if sc_count == 1 then 2 else 4 end - if sc_count == 1 @@ -23,16 +23,8 @@ %label{for:c.id, tabindex:'1' } = link_to(client_name, client_path(c), target: "_blank") - if sc_count == 1 - %td - - if can_view_full_dob? - = c.DOB - - else - = c.age - %td - - if can_view_full_ssn? - = ssn(c.SSN) - - else - = masked_ssn(c.SSN) + %td= c.pii_provider(user: current_user).dob_or_age + %td= c.pii_provider(user: current_user).ssn - else - c.source_clients.each do |sc| %tr @@ -42,11 +34,6 @@ .c-checkbox.c-checkbox.mr-4 = check_box_tag input_id, sc.id, nil, id: sc.id %label{for:sc.id, tabindex:'1' } - %span= "#{sc.full_name} in #{sc.data_source&.short_name}
#{sc.uuid}".html_safe - %td - = sc.DOB - %td - - if can_view_full_ssn? - = ssn(sc.SSN) - - else - = masked_ssn(sc.SSN) + %span= "#{sc.pii_provider(user: current_user).full_name} in #{sc.data_source&.short_name}
#{sc.uuid}".html_safe + %td= sc.pii_provider(user: current_user).dob_or_age + %td= sc.pii_provider(user: current_user).ssn diff --git a/app/views/clients/_new_client_form.haml b/app/views/clients/_new_client_form.haml index 170c53aee33..ea7a43ba8f1 100644 --- a/app/views/clients/_new_client_form.haml +++ b/app/views/clients/_new_client_form.haml @@ -18,6 +18,7 @@ %th SSN %tbody - @existing_matches.each do |client| + - pii = client.pii_provider(user: current_user) %tr %td - ds_id = client.data_source.id @@ -30,11 +31,9 @@ - else - link = client.destination_client.appropriate_path_for?(current_user) = link_to link do - = client.full_name - %td - = client.DOB - %td - = client.SSN + = pii.full_name + %td= pii.dob_or_age + %td= pii.ssn %h2 New Client %p If none of the above match the client you are attempting to add, click diff --git a/app/views/clients/_potential_matches.html.haml b/app/views/clients/_potential_matches.html.haml index fd6d2627887..5ffdfcb1817 100644 --- a/app/views/clients/_potential_matches.html.haml +++ b/app/views/clients/_potential_matches.html.haml @@ -1,7 +1,7 @@ %h3 Potential Matches -%p - This section allows you to merge a client into - = "#{@client.name}." +%p + This section allows you to merge a client into + = "#{@client.pii_provider(user: current_user).full_name}." If a potential client is the combination of merged clients, you can choose to merge with the client set, or with an individual client. - if @potential_matches.any? .row @@ -12,7 +12,6 @@ = k.to_s.humanize.titlecase = render 'match_results', f: f, clients: clients .form-actions - = f.button :submit, "Merge into #{@client.FirstName} #{@client.LastName}" + = f.button :submit, "Merge into #{@client.pii_provider(user: current_user).full_name}" - else %p No potential matches found - \ No newline at end of file diff --git a/app/views/clients/_unmerge_form.haml b/app/views/clients/_unmerge_form.haml index fa6f788a0b3..633a9d97662 100644 --- a/app/views/clients/_unmerge_form.haml +++ b/app/views/clients/_unmerge_form.haml @@ -30,13 +30,14 @@ %th Data Source %th Personal ID %th Name - %th DOB + %th DOB/Age %th SSN %th Merged %th Merged By %th %tbody - source_clients.each do |c| + - pii = c.pii_provider(user: current_user) %tr %td.jSplit= f.input_field :merge, as: :boolean, checked_value: c.id, unchecked_value: nil, name: input_id - if GrdaWarehouse::Config.get(:healthcare_available) @@ -46,9 +47,9 @@ = radio_button_tag 'grda_warehouse_hud_client[health_receiver]', c.id, false, disabled: true %td= c.data_source&.short_name %td= c.uuid - %td= c.full_name - %td= c.DOB - %td= ssn c.SSN + %td= pii.full_name + %td= pii.dob_or_age + %td= pii.ssn %td= c.warehouse_client_source.reviewed_at %td - if (user = reviewers[c.warehouse_client_source.reviewd_by.to_i]) @@ -59,7 +60,7 @@ Match Details .form-actions - = f.button :submit, "Split selected records from #{@client.FirstName} #{@client.LastName}", id: :splitButton, data: {confirm: "Are you sure you want to split #{@client.FirstName} #{@client.LastName}? Dependent data will be moved to the selected clients."} + = f.button :submit, "Split selected records from #{@client.pii_provider(user: current_user).full_name}", id: :splitButton, data: {confirm: "Are you sure you want to split #{@client.pii_provider(user: current_user).full_name}? Dependent data will be moved to the selected clients."} = content_for :page_js do :javascript diff --git a/app/views/clients/anomalies/_breadcrumbs.haml b/app/views/clients/anomalies/_breadcrumbs.haml index 9a8d124b593..515045c1a6e 100644 --- a/app/views/clients/anomalies/_breadcrumbs.haml +++ b/app/views/clients/anomalies/_breadcrumbs.haml @@ -1,4 +1,4 @@ = content_for :crumbs do = link_to client_path(@client) do - « Back to - = @client.name \ No newline at end of file + « Back to + = @client.pii_provider(user: current_user).full_name diff --git a/app/views/clients/cas_readiness/_active_clients.haml b/app/views/clients/cas_readiness/_active_clients.haml index 7225e09e9b9..240c354c5de 100644 --- a/app/views/clients/cas_readiness/_active_clients.haml +++ b/app/views/clients/cas_readiness/_active_clients.haml @@ -2,7 +2,7 @@ Any client with service within the past #{pluralize(GrdaWarehouse::Config.get(:cas_sync_months), 'month')}. %p Will - %strong= @client.name + %strong= @client.pii_provider(user: current_user).full_name sync with CAS? %br = yes_no @client.active_in_cas?(include_overridden: false) diff --git a/app/views/clients/cas_readiness/_ce_with_assessment.haml b/app/views/clients/cas_readiness/_ce_with_assessment.haml index 7fa1173e41c..f7e5febf350 100644 --- a/app/views/clients/cas_readiness/_ce_with_assessment.haml +++ b/app/views/clients/cas_readiness/_ce_with_assessment.haml @@ -2,7 +2,7 @@ Any client currently enrolled in a Coordinated Entry project and having at least one associated assessment. %p Will - %strong= @client.name + %strong= @client.pii_provider(user: current_user).full_name sync with CAS? %br = yes_no @client.active_in_cas?(include_overridden: false) diff --git a/app/views/clients/cas_readiness/_chronic.haml b/app/views/clients/cas_readiness/_chronic.haml index 3a743080355..1ecb5cba319 100644 --- a/app/views/clients/cas_readiness/_chronic.haml +++ b/app/views/clients/cas_readiness/_chronic.haml @@ -2,7 +2,7 @@ All clients on the most recent potentially chronic list will be included in each CAS sync. %p Will - %strong= @client.name + %strong= @client.pii_provider(user: current_user).full_name sync with CAS? %br = yes_no @client.active_in_cas?(include_overridden: false) diff --git a/app/views/clients/cas_readiness/_hud_chronic.haml b/app/views/clients/cas_readiness/_hud_chronic.haml index 4aa7113954c..f698cfc4c60 100644 --- a/app/views/clients/cas_readiness/_hud_chronic.haml +++ b/app/views/clients/cas_readiness/_hud_chronic.haml @@ -2,7 +2,7 @@ All clients on the most recent chronic list will be included in each CAS sync. %p Will - %strong= @client.name + %strong= @client.pii_provider(user: current_user).full_name sync with CAS? %br = yes_no @client.active_in_cas?(include_overridden: false) diff --git a/app/views/clients/cas_readiness/_project_group.haml b/app/views/clients/cas_readiness/_project_group.haml index d53aba4f82a..404673e684a 100644 --- a/app/views/clients/cas_readiness/_project_group.haml +++ b/app/views/clients/cas_readiness/_project_group.haml @@ -2,7 +2,7 @@ Any client in project group '#{GrdaWarehouse::Config.cas_sync_project_group.name}'. %p Will - %strong= @client.name + %strong= @client.pii_provider(user: current_user).full_name sync with CAS? %br = yes_no @client.active_in_cas?(include_overridden: false) diff --git a/app/views/clients/cas_readiness/_release_and_project_group.haml b/app/views/clients/cas_readiness/_release_and_project_group.haml index abfe4ba2a33..8dc68b5c6d1 100644 --- a/app/views/clients/cas_readiness/_release_and_project_group.haml +++ b/app/views/clients/cas_readiness/_release_and_project_group.haml @@ -10,7 +10,7 @@ will sync with CAS %p Will - %strong= @client.name + %strong= @client.pii_provider(user: current_user).full_name sync with CAS? %br = yes_no @client.active_in_cas?(include_overridden: false) diff --git a/app/views/clients/cas_readiness/_release_present.haml b/app/views/clients/cas_readiness/_release_present.haml index 4d133c72adc..b7377e211c0 100644 --- a/app/views/clients/cas_readiness/_release_present.haml +++ b/app/views/clients/cas_readiness/_release_present.haml @@ -5,7 +5,7 @@ %p Will - %strong= @client.name + %strong= @client.pii_provider(user: current_user).full_name sync with CAS? %br = yes_no @client.active_in_cas?(include_overridden: false) diff --git a/app/views/clients/coordinated_entry_assessments/edit.haml b/app/views/clients/coordinated_entry_assessments/edit.haml index 18e51e50c88..8dc673f27b9 100644 --- a/app/views/clients/coordinated_entry_assessments/edit.haml +++ b/app/views/clients/coordinated_entry_assessments/edit.haml @@ -1,4 +1,4 @@ -- title = "Editing #{_'Coordinated Entry Assessment'} for #{@client.name}" +- title = "Editing #{_'Coordinated Entry Assessment'} for #{@client.pii_provider(user: current_user).full_name}" - content_for :title, title .o-page = render 'clients/breadcrumbs' @@ -11,4 +11,4 @@ = render 'clients/tab_navigation', current: client_coordinated_entry_assessments_path(client_id: @client) = simple_form_for @assessment, url: client_coordinated_entry_assessment_path(@client, @assessment) do |f| - = render 'form', f: f, readonly: false \ No newline at end of file + = render 'form', f: f, readonly: false diff --git a/app/views/clients/coordinated_entry_assessments/new.haml b/app/views/clients/coordinated_entry_assessments/new.haml index ba4e987cfab..f49a275a5b5 100644 --- a/app/views/clients/coordinated_entry_assessments/new.haml +++ b/app/views/clients/coordinated_entry_assessments/new.haml @@ -1,4 +1,4 @@ -- title = "New #{_'Coordinated Entry Assessment'} for #{@client.name}" +- title = "New #{_'Coordinated Entry Assessment'} for #{@client.pii_provider(user: current_user).full_name}" - content_for :title, title .o-page = render 'clients/breadcrumbs' @@ -11,4 +11,4 @@ = render 'clients/tab_navigation', current: client_coordinated_entry_assessments_path(client_id: @client) = simple_form_for @assessment, url: client_coordinated_entry_assessments_path(@client) do |f| - = render 'form', f: f, readonly: false \ No newline at end of file + = render 'form', f: f, readonly: false diff --git a/app/views/clients/coordinated_entry_assessments/show.haml b/app/views/clients/coordinated_entry_assessments/show.haml index c264a6336a2..ec8d3e5ae69 100644 --- a/app/views/clients/coordinated_entry_assessments/show.haml +++ b/app/views/clients/coordinated_entry_assessments/show.haml @@ -1,4 +1,4 @@ -- title = "#{_'Coordinated Entry Assessment'} for #{@client.name}" +- title = "#{_'Coordinated Entry Assessment'} for #{@client.pii_provider(user: current_user).full_name}" - content_for :title, title .o-page = render 'clients/breadcrumbs' @@ -11,4 +11,4 @@ = render 'clients/tab_navigation', current: client_coordinated_entry_assessments_path(client_id: @client) = simple_form_for @assessment, wrapper: :readonly, url: client_coordinated_entry_assessment_path(@client, @assessment) do |f| - = render 'form', f: f \ No newline at end of file + = render 'form', f: f diff --git a/app/views/clients/edit.haml b/app/views/clients/edit.haml index 23aef9ff723..1bdbdf64ebf 100644 --- a/app/views/clients/edit.haml +++ b/app/views/clients/edit.haml @@ -1,4 +1,4 @@ -- title = "Potential Duplicates for #{@client.name}" +- title = "Potential Duplicates for #{@client.pii_provider(user: current_user).full_name}" - content_for :title, title = render 'breadcrumbs' diff --git a/app/views/clients/enrollment_history/index.haml b/app/views/clients/enrollment_history/index.haml index cea0bd859d1..953bfe67ded 100644 --- a/app/views/clients/enrollment_history/index.haml +++ b/app/views/clients/enrollment_history/index.haml @@ -1,4 +1,4 @@ -- title = "Historical Enrollment Data for #{@client.name} on #{@date}" +- title = "Historical Enrollment Data for #{@client.pii_provider(user: current_user).full_name} on #{@date}" - content_for :title, title %h1= title = render 'clients/anomalies/breadcrumbs' diff --git a/app/views/clients/enrollments/show.haml b/app/views/clients/enrollments/show.haml index e1b8d7f1af2..51255f7591a 100644 --- a/app/views/clients/enrollments/show.haml +++ b/app/views/clients/enrollments/show.haml @@ -1,4 +1,4 @@ -- title = "Enrollment at #{@enrollment.project&.name(current_user)} for #{@client.name} " +- title = "Enrollment at #{@enrollment.project&.name(current_user)} for #{@client.pii_provider(user: current_user).full_name} " - content_for :title, title = render 'clients/anomalies/breadcrumbs' %h1= title diff --git a/app/views/clients/files/_permission_warning.haml b/app/views/clients/files/_permission_warning.haml index 1356e31ce68..89784512f75 100644 --- a/app/views/clients/files/_permission_warning.haml +++ b/app/views/clients/files/_permission_warning.haml @@ -3,8 +3,8 @@ %i.alert__icon.icon-info %p - if can_manage_window_client_files? && @client.release_valid?(coc_codes: current_user.coc_codes) - = Translation.translate("#{@client.name} has a consent form on file, you can see any client files.") + = Translation.translate("#{@client.pii_provider(user: current_user).full_name} has a consent form on file, you can see any client files.") - elsif can_manage_window_client_files? - = Translation.translate("#{@client.name} does not have a consent form on file, you can only see files you upload.") + = Translation.translate("#{@client.pii_provider(user: current_user).full_name} does not have a consent form on file, you can only see files you upload.") - else = Translation.translate("You can only see files you upload.") diff --git a/app/views/clients/notes/index.haml b/app/views/clients/notes/index.haml index 4a4cf4373c6..7c35de849da 100644 --- a/app/views/clients/notes/index.haml +++ b/app/views/clients/notes/index.haml @@ -13,9 +13,9 @@ - unless can_edit_client_notes? || can_view_all_window_notes? %p.alert.alert-info - if can_edit_window_client_notes? && @client.release_valid? - = Translation.translate("#{@client.name} has a consent form on file, you can see any notes about this client.") + = Translation.translate("#{@client.pii_provider(user: current_user).full_name} has a consent form on file, you can see any notes about this client.") - elsif can_edit_window_client_notes? - = Translation.translate("#{@client.name} does not have a consent form on file, you can only see notes you add.") + = Translation.translate("#{@client.pii_provider(user: current_user).full_name} does not have a consent form on file, you can only see notes you add.") - else = Translation.translate("You can only see notes you add.") - if @notes.any? diff --git a/app/views/clients/releases/_form.haml b/app/views/clients/releases/_form.haml index 8d1af43786f..b31e4b51742 100644 --- a/app/views/clients/releases/_form.haml +++ b/app/views/clients/releases/_form.haml @@ -3,7 +3,7 @@ .col-4 %dl %dt Name: - %dd= @client.name + %dd= @client.pii_provider(user: current_user).full_name .col-4 %dl %dt Date of Birth: diff --git a/app/views/clients/rollup/_family.html.haml b/app/views/clients/rollup/_family.html.haml index ad8df00eb30..75668a179bf 100644 --- a/app/views/clients/rollup/_family.html.haml +++ b/app/views/clients/rollup/_family.html.haml @@ -9,14 +9,11 @@ %th Race %tbody - @client.family_members.each do |client| + - pii = client.pii_provider(user: current_user) %tr %td - = link_to client.name, client_path(client) - %td - - if can_view_full_ssn? - = ssn(client.SSN) - - else - = masked_ssn(client.SSN) + = link_to pii.full_name, client_path(client) + %td= pii.ssn %td= client.age %td= client.gender %td= client.race_fields.map{ |f| HudUtility2024.race(f) }.join ', ' diff --git a/app/views/clients/simple.haml b/app/views/clients/simple.haml index df60c6900fc..ec78b27dfdb 100644 --- a/app/views/clients/simple.haml +++ b/app/views/clients/simple.haml @@ -1,5 +1,5 @@ -- title = @client.full_name +- title = @client.pii_provider(user: current_user).full_name - content_for :title, title = render 'breadcrumbs' = render 'tab_navigation', current: simple_client_path(@client) -= render 'client_card', client: @client, disable_link: true \ No newline at end of file += render 'client_card', client: @client, disable_link: true diff --git a/app/views/clients/users/index.haml b/app/views/clients/users/index.haml index aeaae794b39..263b4ae07ed 100644 --- a/app/views/clients/users/index.haml +++ b/app/views/clients/users/index.haml @@ -1,8 +1,8 @@ -- title = "Assigned Relationships for #{@client.name}" +- title = "Assigned Relationships for #{@client.pii_provider(user: current_user).full_name}" - content_for :title, title = render 'clients/breadcrumbs' = render 'clients/aliases' = render 'clients/tab_navigation', current: client_users_path(@client) -= render 'relationship_list' \ No newline at end of file += render 'relationship_list' diff --git a/app/views/clients/vispdats/_list.haml b/app/views/clients/vispdats/_list.haml index 392660088f4..93e53278015 100644 --- a/app/views/clients/vispdats/_list.haml +++ b/app/views/clients/vispdats/_list.haml @@ -21,7 +21,7 @@ %i.icon-group Family -# else - %button.btn.btn-primary(title="#{@client.full_name} already has a VI-SPDAT in progress. Please complete that one or delete it before starting a new VI-SPDAT" data-toggle='tooltip') + %button.btn.btn-primary(title="#{@client.pii_provider(user: current_user).full_name} already has a VI-SPDAT in progress. Please complete that one or delete it before starting a new VI-SPDAT" data-toggle='tooltip') %span.icon-plus .card diff --git a/app/views/clients/vispdats/edit.haml b/app/views/clients/vispdats/edit.haml index 097e887f063..3bc62aecb44 100644 --- a/app/views/clients/vispdats/edit.haml +++ b/app/views/clients/vispdats/edit.haml @@ -16,7 +16,7 @@ %span.icon-eye View - if can_edit_vspdat? - = link_to polymorphic_path(vispdat_path_generator, client_id: @client.id, id: @vispdat.id), class: 'btn btn-danger', title: 'Delete', method: :delete, data: { toggle: 'tooltip', confirm: "Are you sure you want to delete the VI-SPDAT for #{@client.full_name}?" } do + = link_to polymorphic_path(vispdat_path_generator, client_id: @client.id, id: @vispdat.id), class: 'btn btn-danger', title: 'Delete', method: :delete, data: { toggle: 'tooltip', confirm: "Are you sure you want to delete the VI-SPDAT for #{@client.pii_provider(user: current_user).full_name}?" } do %span.icon-cross Delete = link_to polymorphic_path(vispdats_path_generator, client_id: @client.id), class: 'btn btn-default', title: 'All', data: { toggle: 'tooltip' } do diff --git a/app/views/clients/vispdats/new.haml b/app/views/clients/vispdats/new.haml index 0cbf163dea3..cc8cc0b06d8 100644 --- a/app/views/clients/vispdats/new.haml +++ b/app/views/clients/vispdats/new.haml @@ -1,4 +1,4 @@ -- title = "New VISPDAT for #{@client.name}" +- title = "New VISPDAT for #{@client.pii_provider(user: current_user).full_name}" - content_for :title, title .o-page = render 'clients/breadcrumbs' diff --git a/app/views/clients/vispdats/show.haml b/app/views/clients/vispdats/show.haml index 561227be0fe..1f69c8ce0c8 100644 --- a/app/views/clients/vispdats/show.haml +++ b/app/views/clients/vispdats/show.haml @@ -1,4 +1,4 @@ -- content_for(:title, @client.name) +- content_for(:title, @client.pii_provider(user: current_user).full_name) .o-page = render 'clients/breadcrumbs' @@ -20,7 +20,7 @@ %span.icon-pencil Edit - if can_edit_vspdat? - = link_to polymorphic_path(vispdat_path_generator, client_id: @client.id, id: @vispdat.id), class: 'btn btn-danger', title: 'Delete', method: :delete, data: { confirm: "Are you sure you want to delete the VI-SPDAT for #{@client.full_name}?" } do + = link_to polymorphic_path(vispdat_path_generator, client_id: @client.id, id: @vispdat.id), class: 'btn btn-danger', title: 'Delete', method: :delete, data: { confirm: "Are you sure you want to delete the VI-SPDAT for #{@client.pii_provider(user: current_user).full_name}?" } do %span.icon-cross Delete = link_to polymorphic_path(vispdats_path_generator, client_id: @client.id), class: 'btn btn-default', title: 'All', data: { toggle: 'tooltip' } do diff --git a/app/views/clients/youth/housing_resolution_plans/_form.haml b/app/views/clients/youth/housing_resolution_plans/_form.haml index 4a5034f8954..8b2059d50c2 100644 --- a/app/views/clients/youth/housing_resolution_plans/_form.haml +++ b/app/views/clients/youth/housing_resolution_plans/_form.haml @@ -1,11 +1,11 @@ .row .col-6 - = f.input 'name', as: :string, disabled: true, label: 'YYA Name (goes by)', input_html: {value: @client.name} + = f.input 'name', as: :string, disabled: true, label: 'YYA Name (goes by)', input_html: {value: @client.pii_provider(user: current_user).full_name} .col-6 = f.input :pronouns, label: 'YYA Pronouns' .row .col-6 - = f.input 'name', as: :string, disabled: true, label: 'YYA Legal Name', input_html: {value: @client.name} + = f.input 'name', as: :string, disabled: true, label: 'YYA Legal Name', input_html: {value: @client.pii_provider(user: current_user).full_name} .col-6 = f.input :planned_on, as: :date_picker, label: 'Date' .row diff --git a/app/views/clients/youth/intakes/_breadcrumbs.haml b/app/views/clients/youth/intakes/_breadcrumbs.haml index 21ef3bf25ce..0bbee9443b4 100644 --- a/app/views/clients/youth/intakes/_breadcrumbs.haml +++ b/app/views/clients/youth/intakes/_breadcrumbs.haml @@ -1,4 +1,4 @@ = content_for :crumbs do = link_to polymorphic_path(youth_intakes_path_generator) do « - = Translation.translate("Youth Intakes for #{@client.name}") + = Translation.translate("Youth Intakes for #{@client.pii_provider(user: current_user).full_name}") diff --git a/app/views/clients/youth/intakes/_intakes.haml b/app/views/clients/youth/intakes/_intakes.haml index 7400e6b2703..0a34f11066b 100644 --- a/app/views/clients/youth/intakes/_intakes.haml +++ b/app/views/clients/youth/intakes/_intakes.haml @@ -1,11 +1,11 @@ - if @client.youth_follow_up_due_soon? %div.alert.alert-warning %i.alert__icon.icon-warning - A 3-month follow up case note is due for #{@client.name} on #{@client.youth_follow_up_due_on} + A 3-month follow up case note is due for #{@client.pii_provider(user: current_user).full_name} on #{@client.youth_follow_up_due_on} - if @client.youth_follow_up_due? %div.alert.alert-danger %i.alert__icon.icon-warning - A 3-month follow up case note was due for #{@client.name} on #{@client.youth_follow_up_due_on} + A 3-month follow up case note was due for #{@client.pii_provider(user: current_user).full_name} on #{@client.youth_follow_up_due_on} = render 'intake_list' = render 'case_management_list' - if GrdaWarehouse::Config.get(:enable_youth_hrp) diff --git a/app/views/clients/youth/intakes/edit.haml b/app/views/clients/youth/intakes/edit.haml index 574910ba71e..4c8d38dd46d 100644 --- a/app/views/clients/youth/intakes/edit.haml +++ b/app/views/clients/youth/intakes/edit.haml @@ -1,4 +1,4 @@ -- content_for :title, "Edit Youth Intake for #{@client.full_name}" +- content_for :title, "Edit Youth Intake for #{@client.pii_provider(user: current_user).full_name}" %h1.page-title= content_for :title @@ -10,4 +10,4 @@ %i.icon-cross Cancel .ml-2 - = f.submit 'Save', class: 'btn btn-primary' \ No newline at end of file + = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/clients/youth/intakes/index.haml b/app/views/clients/youth/intakes/index.haml index eadfe5a738e..c870baa8cab 100644 --- a/app/views/clients/youth/intakes/index.haml +++ b/app/views/clients/youth/intakes/index.haml @@ -8,6 +8,6 @@ - if can_delete_youth_intake? .text-right - = link_to remove_all_youth_data_client_youth_intakes_path(@client), method: :delete, class: 'btn btn-danger', data: {confirm: "Are you sure you want to delete all youth information for #{@client.name}? Deleting is non-reversible, and will remove all items on this page. Proceed?"} do + = link_to remove_all_youth_data_client_youth_intakes_path(@client), method: :delete, class: 'btn btn-danger', data: {confirm: "Are you sure you want to delete all youth information for #{@client.pii_provider(user: current_user).full_name}? Deleting is non-reversible, and will remove all items on this page. Proceed?"} do %i.icon-cross - Delete All Youth Data \ No newline at end of file + Delete All Youth Data diff --git a/app/views/clients/youth/intakes/new.haml b/app/views/clients/youth/intakes/new.haml index ba7fea7bee3..a3174847a2f 100644 --- a/app/views/clients/youth/intakes/new.haml +++ b/app/views/clients/youth/intakes/new.haml @@ -1,4 +1,4 @@ -- content_for :title, "New Youth Intake for #{@client.full_name}" +- content_for :title, "New Youth Intake for #{@client.pii_provider(user: current_user).full_name}" %h1.page-title= content_for :title @@ -10,4 +10,4 @@ %i.icon-cross Cancel .ml-2 - = f.submit 'Save', class: 'btn btn-primary' \ No newline at end of file + = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/clients/youth/intakes/show.haml b/app/views/clients/youth/intakes/show.haml index a0a1b221785..0be6727876e 100644 --- a/app/views/clients/youth/intakes/show.haml +++ b/app/views/clients/youth/intakes/show.haml @@ -1,5 +1,5 @@ = render 'breadcrumbs' -- content_for :title, "Youth Intake for #{@client.full_name}" +- content_for :title, "Youth Intake for #{@client.pii_provider(user: current_user).full_name}" .d-flex %h1.page-title= content_for :title @@ -10,4 +10,4 @@ Edit = simple_form_for @intake, url: polymorphic_path(youth_intake_path_generator, id: @intake.id), wrapper: :readonly do |f| - = render 'intake_form', f: f, readonly: true \ No newline at end of file + = render 'intake_form', f: f, readonly: true diff --git a/app/views/he/clients/boston_covid_19.haml b/app/views/he/clients/boston_covid_19.haml index 78709e5315b..b9880a29b13 100644 --- a/app/views/he/clients/boston_covid_19.haml +++ b/app/views/he/clients/boston_covid_19.haml @@ -1,4 +1,4 @@ -- title = @client.full_name +- title = @client.pii_provider(user: current_user).full_name - content_for :title, title = render 'clients/breadcrumbs' diff --git a/app/views/he/clients/boston_covid_19/_isolation_form.haml b/app/views/he/clients/boston_covid_19/_isolation_form.haml index 05a5ada702b..78f6fca3c83 100644 --- a/app/views/he/clients/boston_covid_19/_isolation_form.haml +++ b/app/views/he/clients/boston_covid_19/_isolation_form.haml @@ -2,7 +2,7 @@ .row - if can_edit_health_emergency_clinical? .col-md-8 - = f.input :isolation_requested_at, as: :string, label: "When was #{@client.name} asked to isolate?", input_html: { class: :date_time_picker } + = f.input :isolation_requested_at, as: :string, label: "When was #{@client.pii_provider(user: current_user).full_name} asked to isolate?", input_html: { class: :date_time_picker } = f.input :location, label: 'Isolation location', collection: @isolation.location_options, input_html: { data: {tags: true} }, as: :select_two = f.input :started_on, as: :date_picker, label: 'Isolation start date' = f.input :scheduled_to_end_on, as: :date_picker, label: 'Scheduled isolation end date' @@ -14,4 +14,4 @@ = render "he/clients/#{health_emergency}/previous_isolation" - elsif can_see_health_emergency_clinical? .col - = render "he/clients/#{health_emergency}/previous_isolation" \ No newline at end of file + = render "he/clients/#{health_emergency}/previous_isolation" diff --git a/app/views/he/clients/boston_covid_19/_quarantine_form.haml b/app/views/he/clients/boston_covid_19/_quarantine_form.haml index d3ad642078f..c94f227130b 100644 --- a/app/views/he/clients/boston_covid_19/_quarantine_form.haml +++ b/app/views/he/clients/boston_covid_19/_quarantine_form.haml @@ -2,7 +2,7 @@ .row - if can_edit_health_emergency_clinical? .col-md-8 - = f.input :isolation_requested_at, as: :string, label: "When was #{@client.name} asked to quarantine?", input_html: { class: :date_time_picker } + = f.input :isolation_requested_at, as: :string, label: "When was #{@client.pii_provider(user: current_user).full_name} asked to quarantine?", input_html: { class: :date_time_picker } = f.input :location, label: 'Quarantine location', collection: @quarantine.location_options, input_html: { data: {tags: true} }, as: :select_two = f.input :started_on, as: :date_picker, label: 'Quarantine start date' = f.input :scheduled_to_end_on, as: :date_picker, label: 'Scheduled quarantine end date' @@ -15,4 +15,3 @@ - elsif can_see_health_emergency_clinical? .col = render "he/clients/#{health_emergency}/previous_quarantine" - diff --git a/app/views/he/clients/boston_covid_19/_triage_form.haml b/app/views/he/clients/boston_covid_19/_triage_form.haml index c9efac15838..ea87c524666 100644 --- a/app/views/he/clients/boston_covid_19/_triage_form.haml +++ b/app/views/he/clients/boston_covid_19/_triage_form.haml @@ -4,8 +4,8 @@ .col-md-8 = f.input :agency, label: 'Where do you work?', collection: Agency.all, as: :select_two, selected: @triage.agency&.id = f.input :location, label: 'Where is this being collected?' - = f.input :exposure, collection: @triage.exposure_options, include_blank: '', label: "Has #{@client.name} been exposed?", as: :boolean_button_group - = f.input :symptoms, collection: @triage.symptom_options, include_blank: '', label: "Does #{@client.name} have symptoms?", as: :boolean_button_group + = f.input :exposure, collection: @triage.exposure_options, include_blank: '', label: "Has #{@client.pii_provider(user: current_user).full_name} been exposed?", as: :boolean_button_group + = f.input :symptoms, collection: @triage.symptom_options, include_blank: '', label: "Does #{@client.pii_provider(user: current_user).full_name} have symptoms?", as: :boolean_button_group = f.input :first_symptoms_on, label: 'First symptom date', as: :date_picker = f.input :referred_on, as: :date_picker = f.input :referred_to @@ -16,4 +16,4 @@ = render "he/clients/#{health_emergency}/previous_triage" - elsif can_see_health_emergency_screening? .col - = render "he/clients/#{health_emergency}/previous_triage" \ No newline at end of file + = render "he/clients/#{health_emergency}/previous_triage" diff --git a/app/views/he/clients/covid_19_vaccinations_only.haml b/app/views/he/clients/covid_19_vaccinations_only.haml index d9a87b94f1f..ff3ef6fd477 100644 --- a/app/views/he/clients/covid_19_vaccinations_only.haml +++ b/app/views/he/clients/covid_19_vaccinations_only.haml @@ -1,4 +1,4 @@ -- title = @client.full_name +- title = @client.pii_provider(user: current_user).full_name - content_for :title, title = render 'clients/breadcrumbs' diff --git a/app/views/projects/_clients.haml b/app/views/projects/_clients.haml index f2ad3995425..2d0b69e9e97 100644 --- a/app/views/projects/_clients.haml +++ b/app/views/projects/_clients.haml @@ -14,7 +14,8 @@ %tr %td - if ! @project.confidential? || can_edit_projects? || ! project.confidential_for_user?(user) - = link_to_if can_view_clients?, service.client.name, client_path(service.client) + - name = service.client.pii_provider(user: current_user).full_name + = link_to_if can_view_clients?, name, client_path(service.client) - else Confidential Client %td= service.first_date_in_program diff --git a/app/views/source_clients/edit.haml b/app/views/source_clients/edit.haml index cb7bf7245a4..2781101db4f 100644 --- a/app/views/source_clients/edit.haml +++ b/app/views/source_clients/edit.haml @@ -1,4 +1,4 @@ -- title = "Edit #{@client.name} at #{@client.data_source.name}" +- title = "Edit #{@client.pii_provider(user: current_user).full_name} at #{@client.data_source.name}" - content_for :modal_title, title = simple_form_for(@client, as: :client, url: polymorphic_path(source_client_path_generator, id: @client.id)) do |f| .row diff --git a/app/views/source_data/show.haml b/app/views/source_data/show.haml index e61de59b954..62b6b0a22a3 100644 --- a/app/views/source_data/show.haml +++ b/app/views/source_data/show.haml @@ -1,4 +1,10 @@ - title = "HMIS #{@type} Source Data" + +- pii = nil +- redact = {} +- if @item.class.name == 'GrdaWarehouse::Hud::Client' + - pii = @item.pii_provider(user: current_user) + - content_for :title, title %h1= content_for :title - if @hmis @@ -82,10 +88,36 @@ - value = link_to @item[key], source_data_path(search: {id: @item[key], type: 'Organization'}) - elsif key == :ProjectID && @item.class.name != 'GrdaWarehouse::Hud::Project' - value = link_to @item[key], source_data_path(search: {id: @item[key], type: 'Project'}) + - elsif key == :FirstName && pii + - value = pii.first_name + - elsif key == :MiddleName && pii + - value = pii.middle_name + - elsif key == :LastName && pii + - value = pii.last_name + - elsif key == :SSN && pii + - value = pii.ssn + - elsif key == :DOB && pii + - value = pii.dob_and_age %tr %th= key %td= value - if @imported - %td= @imported[key] + %td + - if key.in?([:FirstName, :MiddleName, :LastName]) + = GrdaWarehouse::PiiProvider.viewable_name(@imported[key], policy: pii.policy) + - elsif key.in?([:SSN]) + = GrdaWarehouse::PiiProvider.viewable_ssn(@imported[key], policy: pii.policy) + - elsif key.in?([:DOB]) + = GrdaWarehouse::PiiProvider.viewable_dob(@imported[key], policy: pii.policy) + - else + = @imported[key] - if @csv - %td= @csv[key] + %td + - if key.in?([:FirstName, :MiddleName, :LastName]) + = GrdaWarehouse::PiiProvider.viewable_name(@csv[key], policy: pii.policy) + - elsif key.in?([:SSN]) + = GrdaWarehouse::PiiProvider.viewable_ssn(@csv[key], policy: pii.policy) + - elsif key.in?([:DOB]) + = GrdaWarehouse::PiiProvider.viewable_dob(@csv[key], policy: pii.policy) + - else + = @csv[key] diff --git a/app/views/warehouse_reports/anomalies/_section.haml b/app/views/warehouse_reports/anomalies/_section.haml index eef30ea8d66..0455b3ccd64 100644 --- a/app/views/warehouse_reports/anomalies/_section.haml +++ b/app/views/warehouse_reports/anomalies/_section.haml @@ -18,7 +18,7 @@ %tr %td - if anomaly.client.present? - = link_to_if can_view_clients?, anomaly.client.name, appropriate_client_path(anomaly.client) + = link_to_if can_view_clients?, anomaly.client.pii_provider(user: current_user).full_name, appropriate_client_path(anomaly.client) - else Client no longer available %td diff --git a/app/views/warehouse_reports/cas/health_prioritization/index.haml b/app/views/warehouse_reports/cas/health_prioritization/index.haml index 50d35d82a6c..207c8bb183f 100644 --- a/app/views/warehouse_reports/cas/health_prioritization/index.haml +++ b/app/views/warehouse_reports/cas/health_prioritization/index.haml @@ -30,7 +30,7 @@ %tbody - @clients.each do |client| %tr - %td= link_to_if can_view_clients?, client.name, appropriate_client_path(client) + %td= link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) %td= client.age %td.text-center - disability = @disabilities.key?(client.id) diff --git a/app/views/warehouse_reports/cas/health_prioritization/index.xlsx.axlsx b/app/views/warehouse_reports/cas/health_prioritization/index.xlsx.axlsx index 777cd92682d..ff67175fd1a 100644 --- a/app/views/warehouse_reports/cas/health_prioritization/index.xlsx.axlsx +++ b/app/views/warehouse_reports/cas/health_prioritization/index.xlsx.axlsx @@ -30,7 +30,7 @@ wb.add_worksheet(name: 'Health Prioritization') do |sheet| row = [ client.id, - ::GrdaWarehouse::Config.get(:include_pii_in_detail_downloads) ? client.name : 'Redacted', + ::GrdaWarehouse::Config.get(:include_pii_in_detail_downloads) ? client.pii_provider(user: current_user).full_name : 'Redacted', client.age, disability, vispdat_disability, diff --git a/app/views/warehouse_reports/ce_assessments/index.haml b/app/views/warehouse_reports/ce_assessments/index.haml index 04f931d22fd..4772843c0dd 100644 --- a/app/views/warehouse_reports/ce_assessments/index.haml +++ b/app/views/warehouse_reports/ce_assessments/index.haml @@ -21,9 +21,10 @@ %th Completed? %tbody - @clients.each do |client| + - pii = client.pii_provider(user: current_user) %tr - %td= link_to_if can_view_clients?, client.name, appropriate_client_path(client) - %td= client.DOB + %td= link_to_if can_view_clients?, pii.full_name, appropriate_client_path(client) + %td= pii.dob_or_age %td= client.ce_assessment.location %td= client.ce_assessment.score %td= client.ce_assessment.vulnerability_score diff --git a/app/views/warehouse_reports/conflicting_client_attributes/index.haml b/app/views/warehouse_reports/conflicting_client_attributes/index.haml index 93a9d9cc308..13b98959500 100644 --- a/app/views/warehouse_reports/conflicting_client_attributes/index.haml +++ b/app/views/warehouse_reports/conflicting_client_attributes/index.haml @@ -24,10 +24,11 @@ %th SSN %tbody - @clients.each do |client| + - pii = client.pii_provider(user: current_user) %tr - %td= link_to_if can_view_clients?, client.name, appropriate_client_path(client) - %td= dob_or_age client.DOB - %td= ssn client.SSN + %td= link_to_if can_view_clients?, pii.full_name, appropriate_client_path(client) + %td= pii.dob_or_age + %td= pii.ssn = render 'common/pagination_bottom', item_name: 'client' - else .none-found No clients found. diff --git a/app/views/warehouse_reports/consent/index.haml b/app/views/warehouse_reports/consent/index.haml index c1cd7bf1763..8bcbdc5682c 100644 --- a/app/views/warehouse_reports/consent/index.haml +++ b/app/views/warehouse_reports/consent/index.haml @@ -41,7 +41,7 @@ - columns_to_skip = @cohorts_for_unconfirmed.count %tr %td.pl-2 - = link_to_if can_view_clients?, client.full_name, appropriate_client_path(client) + = link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) %td.text-center - if current_user.can_confirm_housing_release? - if client.active_in_cas? diff --git a/app/views/warehouse_reports/dob_entry_same/index.haml b/app/views/warehouse_reports/dob_entry_same/index.haml index 25c3b27e6f3..9af6d7dfd79 100644 --- a/app/views/warehouse_reports/dob_entry_same/index.haml +++ b/app/views/warehouse_reports/dob_entry_same/index.haml @@ -19,10 +19,11 @@ %th Project %tbody - @clients.each do |client| + - pii = client.pii_provider(user: current_user) - enrollment = client.source_enrollments.select{ |m| m[:EntryDate] == client.DOB }.first %tr - %td= link_to_if can_view_clients?, client.name, appropriate_client_path(client) - %td= client.DOB + %td= link_to_if can_view_clients?, pii.full_name, appropriate_client_path(client) + %td= pii.dob_or_age %td= enrollment&.EntryDate %td= enrollment&.project&.name(current_user) = render 'common/pagination_bottom', item_name: 'client' diff --git a/app/views/warehouse_reports/dv_victim_service/_table.haml b/app/views/warehouse_reports/dv_victim_service/_table.haml index c31045d4515..ceb82b0c7b9 100644 --- a/app/views/warehouse_reports/dv_victim_service/_table.haml +++ b/app/views/warehouse_reports/dv_victim_service/_table.haml @@ -13,7 +13,7 @@ - client = source_client.destination_client %tr %td= client.id - %td= link_to_if can_view_clients?, source_client.name, appropriate_client_path(client.id) + %td= link_to_if can_view_clients?, source_client.pii_provider(user: current_user).full_name, appropriate_client_path(client.id) = render 'common/pagination_bottom', item_name: 'client' - else .none-found No clients found. diff --git a/app/views/warehouse_reports/expiring_consent/index.haml b/app/views/warehouse_reports/expiring_consent/index.haml index bd00e5616d3..7ce21f51d8e 100644 --- a/app/views/warehouse_reports/expiring_consent/index.haml +++ b/app/views/warehouse_reports/expiring_consent/index.haml @@ -18,7 +18,7 @@ - @unconfirmed.each do |client| %tr %td - = link_to_if can_view_clients?, client.full_name, appropriate_client_path(client) + = link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) %td = client.consent_form_signed_on + client.class.consent_validity_period %td @@ -40,7 +40,7 @@ - @expiring_clients.each do |client| %tr %td - = link_to_if can_view_clients?, client.full_name, appropriate_client_path(client) + = link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) %td = client.consent_form_signed_on + client.class.consent_validity_period %td @@ -62,7 +62,7 @@ - @expired_clients.each do |client| %tr %td - = link_to_if can_view_clients?, client.full_name, appropriate_client_path(client) + = link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) %td = client.consent_form_signed_on + client.class.consent_validity_period %td diff --git a/app/views/warehouse_reports/health_emergency/medical_restrictions/_table.haml b/app/views/warehouse_reports/health_emergency/medical_restrictions/_table.haml index 292c755ed31..0b606471d39 100644 --- a/app/views/warehouse_reports/health_emergency/medical_restrictions/_table.haml +++ b/app/views/warehouse_reports/health_emergency/medical_restrictions/_table.haml @@ -21,15 +21,16 @@ %tbody - @restrictions.each do |restriction| - client = restriction.client + - pii = client.pii_provider(user: current_user) - batch_class = if restriction.in_batch?(params[:batch_id]) then 'report-hightlight' else '' end %tr %td{class: batch_class} - if @html && client.image .client__image{ style: "background-image: url(#{ image_client_path(client) })" } %td= link_to_if @html, client.id, polymorphic_path(['client_he', health_emergency], client_id: client) - %td= link_to_if @html, client.name, polymorphic_path(['client_he', health_emergency], client_id: client) - %td= client.DOB - %td= client.age + %td= link_to_if @html, pii.full_name, polymorphic_path(['client_he', health_emergency], client_id: client) + %td= pii.dob + %td= pii.age %td= restriction.created_at.to_date %td= simple_format(restriction.notes || restriction.note) - if @html diff --git a/app/views/warehouse_reports/inactive_youth_intakes/_table.haml b/app/views/warehouse_reports/inactive_youth_intakes/_table.haml index b2a94619d96..7efd28d22dc 100644 --- a/app/views/warehouse_reports/inactive_youth_intakes/_table.haml +++ b/app/views/warehouse_reports/inactive_youth_intakes/_table.haml @@ -16,7 +16,7 @@ %tbody - @report.clients.each do |client| %tr - %td= link_to client.client.name, client_youth_intakes_path(client.client) + %td= link_to client.client.pii_provider(user: current_user).full_name, client_youth_intakes_path(client.client) %td= client.intake.engagement_date - highlight_class = if client.case_mangement.present? && client.case_mangement == client.max_date then 'table-warning' else '' end %td{class: highlight_class}= client.case_mangement diff --git a/app/views/warehouse_reports/incomes/index.haml b/app/views/warehouse_reports/incomes/index.haml index 380e259001d..617fc4db7f2 100644 --- a/app/views/warehouse_reports/incomes/index.haml +++ b/app/views/warehouse_reports/incomes/index.haml @@ -33,7 +33,7 @@ - @enrollments.each do |record| %tr %td - = link_to_if can_view_clients?, record.client.name, appropriate_client_path(record.client) + = link_to_if can_view_clients?, record.client.pii_provider(user: current_user).full_name, appropriate_client_path(record.client) %td %ul.list-unstyled - record.enrollment.income_benefits_at_entry&.sources_and_amounts&.each do |name, amount| diff --git a/app/views/warehouse_reports/outflow/details.haml b/app/views/warehouse_reports/outflow/details.haml index 019b215cbe0..ef22dde1a3e 100644 --- a/app/views/warehouse_reports/outflow/details.haml +++ b/app/views/warehouse_reports/outflow/details.haml @@ -31,7 +31,7 @@ %td{rowspan: enrollments.count+1} = link_to_if can_view_clients?, client.id, appropriate_client_path(client.id) %td{rowspan: enrollments.count+1} - = link_to_if can_view_clients?, client.name, appropriate_client_path(client.id) + = link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client.id) - enrollments.each do |enrollment| %tr %td= enrollment.project&.name(current_user) diff --git a/app/views/warehouse_reports/recidivism/index.haml b/app/views/warehouse_reports/recidivism/index.haml index b826d420595..e14a82f88c1 100644 --- a/app/views/warehouse_reports/recidivism/index.haml +++ b/app/views/warehouse_reports/recidivism/index.haml @@ -25,7 +25,7 @@ .warehouse-reports__client .warehouse-reports__client-name %h4 - = link_to_if can_view_clients?, client.full_name, appropriate_client_path(client) + = link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) .row .col-sm-6 %h4 PH Enrollments diff --git a/app/views/warehouse_reports/time_homeless_for_exits/_table.haml b/app/views/warehouse_reports/time_homeless_for_exits/_table.haml index 77675c6d78a..103bae6e8e4 100644 --- a/app/views/warehouse_reports/time_homeless_for_exits/_table.haml +++ b/app/views/warehouse_reports/time_homeless_for_exits/_table.haml @@ -15,7 +15,7 @@ %tbody - @report.data.each do |client| %tr - %td= link_to_if can_view_clients?, client.client.name, appropriate_client_path(client.client) + %td= link_to_if can_view_clients?, client.client.pii_provider(user: current_user).full_name, appropriate_client_path(client.client) %td= client.days %td= client.entry_date %td= client.exit_date diff --git a/app/views/warehouse_reports/youth_activity/index.haml b/app/views/warehouse_reports/youth_activity/index.haml index 3e257cdcb63..e139c98959b 100644 --- a/app/views/warehouse_reports/youth_activity/index.haml +++ b/app/views/warehouse_reports/youth_activity/index.haml @@ -12,7 +12,7 @@ - else %p No modifications were made to youth records between #{@filter.start} and #{@filter.end}. - @clients.find_each do |client| - %h3= client.name + %h3= client.pii_provider(user: current_user).full_name .c-card.mb-4 .c-card__content - # We've preloaded all, use ruby select to pick those in range diff --git a/drivers/client_access_control/app/controllers/client_access_control/history_controller.rb b/drivers/client_access_control/app/controllers/client_access_control/history_controller.rb index cfe3154dc12..23b859cd8a6 100644 --- a/drivers/client_access_control/app/controllers/client_access_control/history_controller.rb +++ b/drivers/client_access_control/app/controllers/client_access_control/history_controller.rb @@ -111,7 +111,7 @@ def client_needing_processing?(client: @client) end private def title_for_show - "#{@client.name} - Service History" + "#{@client.pii_provider(user: current_user).full_name} - Service History" end end end diff --git a/drivers/client_access_control/app/views/client_access_control/clients/enrollment_details.haml b/drivers/client_access_control/app/views/client_access_control/clients/enrollment_details.haml index 3d2f8239b52..77049d6cde6 100644 --- a/drivers/client_access_control/app/views/client_access_control/clients/enrollment_details.haml +++ b/drivers/client_access_control/app/views/client_access_control/clients/enrollment_details.haml @@ -1,4 +1,4 @@ -- title = @client.full_name +- title = @client.pii_provider(user: current_user).full_name - content_for :title, title = render 'clients/breadcrumbs' diff --git a/drivers/client_access_control/app/views/client_access_control/clients/simple.haml b/drivers/client_access_control/app/views/client_access_control/clients/simple.haml index a0c0c970fde..fbc99faa19e 100644 --- a/drivers/client_access_control/app/views/client_access_control/clients/simple.haml +++ b/drivers/client_access_control/app/views/client_access_control/clients/simple.haml @@ -1,4 +1,4 @@ -- title = @client.full_name +- title = @client.pii_provider(user: current_user).full_name - content_for :title, title = render 'clients/breadcrumbs' = render 'clients/tab_navigation', current: simple_client_path(@client) diff --git a/drivers/client_documents_report/app/views/client_documents_report/warehouse_reports/reports/index.haml b/drivers/client_documents_report/app/views/client_documents_report/warehouse_reports/reports/index.haml index da834cb937c..59d6a622cea 100644 --- a/drivers/client_documents_report/app/views/client_documents_report/warehouse_reports/reports/index.haml +++ b/drivers/client_documents_report/app/views/client_documents_report/warehouse_reports/reports/index.haml @@ -46,7 +46,7 @@ %tbody - @clients.each do |client| %tr - %td= link_to_if can_view_clients?, client.name, appropriate_client_path(client) + %td= link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) %td= @report.required_documents(client).count %td= @report.optional_documents(client).count %td= @report.overall_documents(client).count diff --git a/drivers/client_location_history/app/models/client_location_history/location.rb b/drivers/client_location_history/app/models/client_location_history/location.rb index 30474f46d46..56c7bcc07de 100644 --- a/drivers/client_location_history/app/models/client_location_history/location.rb +++ b/drivers/client_location_history/app/models/client_location_history/location.rb @@ -47,9 +47,9 @@ def as_marker(user = nil, label_attributes = [:seen_on, :collected_by]) private def name_for_label(user) if user.can_view_clients? - link_for(client_path(client), client.name) + link_for(client_path(client), client.pii_provider(user: current_user).full_name) else - client.name + client.pii_provider(user: current_user).full_name end end diff --git a/drivers/client_location_history/app/views/client_location_history/clients/map.haml b/drivers/client_location_history/app/views/client_location_history/clients/map.haml index 0fe4ca0e339..000679b5638 100644 --- a/drivers/client_location_history/app/views/client_location_history/clients/map.haml +++ b/drivers/client_location_history/app/views/client_location_history/clients/map.haml @@ -1,4 +1,4 @@ -- title = "Location Map for #{@client.name}" +- title = "Location Map for #{@client.pii_provider(user: current_user).full_name}" - content_for :title, title = render 'clients/breadcrumbs' diff --git a/drivers/financial/app/views/financial/clients/rollup/_financial_clients.haml b/drivers/financial/app/views/financial/clients/rollup/_financial_clients.haml index b9a8102820c..b16bb7d4d5b 100644 --- a/drivers/financial/app/views/financial/clients/rollup/_financial_clients.haml +++ b/drivers/financial/app/views/financial/clients/rollup/_financial_clients.haml @@ -17,7 +17,7 @@ %tbody - clients.each do |client| %tr - %td= client.name + %td= client.pii_provider(user: current_user).full_name %td= yes_no(client.head_of_household == 1) %td.date-cell= client.date_of_referral_to_agency&.to_date %td.date-cell= client.date_of_referral_to_wit&.to_date diff --git a/drivers/financial/app/views/financial/clients/show.haml b/drivers/financial/app/views/financial/clients/show.haml index d3e9b3a03af..e091d07f325 100644 --- a/drivers/financial/app/views/financial/clients/show.haml +++ b/drivers/financial/app/views/financial/clients/show.haml @@ -1,4 +1,4 @@ -- title = @client.full_name +- title = @client.pii_provider(user: current_user).full_name - content_for :title, title = render 'clients/breadcrumbs' diff --git a/drivers/inactive_client_report/app/views/inactive_client_report/warehouse_reports/reports/_report.haml b/drivers/inactive_client_report/app/views/inactive_client_report/warehouse_reports/reports/_report.haml index 9dbad18b3da..c2996fe0105 100644 --- a/drivers/inactive_client_report/app/views/inactive_client_report/warehouse_reports/reports/_report.haml +++ b/drivers/inactive_client_report/app/views/inactive_client_report/warehouse_reports/reports/_report.haml @@ -19,10 +19,11 @@ %th Most-Recent CE Assessor %tbody - @clients.each do |client| + - pii = client.pii_provider(user: current_user) - projects = client.last_intentional_contacts( current_user, include_confidential_names: false, include_dates: true).select(&:present?) %tr - %td= link_to_if can_view_clients?, client.name, appropriate_client_path(client) - %td= dob_or_age(client.dob) + %td= link_to_if can_view_clients?, pii.full_name, appropriate_client_path(client) + %td= pii.dob_or_age %td - projects.each do |p| = p diff --git a/drivers/start_date_dq/app/views/start_date_dq/warehouse_reports/reports/index.haml b/drivers/start_date_dq/app/views/start_date_dq/warehouse_reports/reports/index.haml index f4458eefd36..91bffd43496 100644 --- a/drivers/start_date_dq/app/views/start_date_dq/warehouse_reports/reports/index.haml +++ b/drivers/start_date_dq/app/views/start_date_dq/warehouse_reports/reports/index.haml @@ -22,7 +22,7 @@ - @enrollments.each do |row| - client = row.client %tr - %td= link_to_if can_view_clients?, client.name, appropriate_client_path(client) + %td= link_to_if can_view_clients?, client.pii_provider(user: current_user).full_name, appropriate_client_path(client) - @report.column_values(row, current_user).each do |key, value| - next if key == :project_type - if key.in?([:days_between, :days_between_start_and_exit]) From fdcc6431677d258bfb80d5e0b3cd59840be7b4a5 Mon Sep 17 00:00:00 2001 From: Elliot Date: Mon, 3 Feb 2025 19:47:41 -0500 Subject: [PATCH 03/10] Silence Ruby EOL (#5105) --- config/brakeman.ignore | 101 ++++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/config/brakeman.ignore b/config/brakeman.ignore index dbd822df41b..d618ca11a8a 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -156,7 +156,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/grda_warehouse/hud/project.rb", - "line": 322, + "line": 314, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "where(\"#{access_to_project_through_viewable_entities(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)} OR #{access_to_project_through_organization(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)} OR #{access_to_project_through_data_source(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)} OR #{access_to_project_through_coc_codes(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)} OR #{access_to_project_through_project_access_groups(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)}\")", "render_path": null, @@ -249,7 +249,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/bi/view_maintainer.rb", - "line": 396, + "line": 399, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "GrdaWarehouseBase.connection.execute(\"DROP VIEW IF EXISTS #{name}\")", "render_path": null, @@ -364,7 +364,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/bi/view_maintainer.rb", - "line": 416, + "line": 423, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "GrdaWarehouseBase.connection.execute(\"CREATE OR REPLACE VIEW #{name} AS #{sql_definition}\")", "render_path": null, @@ -410,7 +410,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 89, + "line": 93, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_inactive\" => true)", "render_path": null, @@ -433,7 +433,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 77, + "line": 81, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_first_time\" => true)", "render_path": null, @@ -502,7 +502,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 18, + "line": 22, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "spm_leaver(period).where(\"#{period}_period_days_to_return\" => (1..731))", "render_path": null, @@ -571,7 +571,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/hmis_external_apis/app/models/hmis_external_apis/tc_hmis/importers/importer.rb", - "line": 105, + "line": 111, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "Hmis::Hud::Base.connection.exec_query(\"ANALYZE #{table_names.uniq.map do\n Hmis::Hud::Base.connection.quote_table_name(n)\n end.join(\",\")};\")", "render_path": null, @@ -594,7 +594,7 @@ "check_name": "Execute", "message": "Possible command injection", "file": "drivers/hmis/app/models/hmis/form/definition.rb", - "line": 365, + "line": 366, "link": "https://brakemanscanner.org/docs/warning_types/command_injection/", "code": "`No Definition found for System form #{role}`", "render_path": null, @@ -697,7 +697,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 37, + "line": 41, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_entering_housing\" => true)", "render_path": null, @@ -720,7 +720,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 29, + "line": 33, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_caper_leaver\" => true)", "render_path": null, @@ -820,7 +820,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 18, + "line": 22, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "spm_leaver(period).where(\"#{period}_period_days_to_return\" => (1..731))", "render_path": null, @@ -866,7 +866,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 77, + "line": 81, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_first_time\" => true)", "render_path": null, @@ -979,7 +979,7 @@ "check_name": "Execute", "message": "Possible command injection", "file": "app/jobs/worker_status.rb", - "line": 114, + "line": 109, "link": "https://brakemanscanner.org/docs/warning_types/command_injection/", "code": "`curl #{ENV[\"ECS_CONTAINER_METADATA_URI_V4\"]}/task`", "render_path": null, @@ -1025,7 +1025,7 @@ "check_name": "UnsafeReflection", "message": "Unsafe reflection method `constantize` called on model attribute", "file": "drivers/hmis/app/models/hmis/form/definition.rb", - "line": 420, + "line": 431, "link": "https://brakemanscanner.org/docs/warning_types/remote_code_execution/", "code": "{ :SERVICE => ({ :owner_class => \"Hmis::Hud::HmisService\", :permission => :can_edit_enrollments }), :PROJECT => ({ :owner_class => \"Hmis::Hud::Project\", :permission => :can_edit_project_details }), :ORGANIZATION => ({ :owner_class => \"Hmis::Hud::Organization\", :permission => :can_edit_organization }), :CLIENT => ({ :owner_class => \"Hmis::Hud::Client\", :permission => :can_edit_clients }), :FUNDER => ({ :owner_class => \"Hmis::Hud::Funder\", :permission => :can_edit_project_details }), :INVENTORY => ({ :owner_class => \"Hmis::Hud::Inventory\", :permission => :can_edit_project_details }), :PROJECT_COC => ({ :owner_class => \"Hmis::Hud::ProjectCoc\", :permission => :can_edit_project_details }), :HMIS_PARTICIPATION => ({ :owner_class => \"Hmis::Hud::HmisParticipation\", :permission => :can_edit_project_details }), :CE_PARTICIPATION => ({ :owner_class => \"Hmis::Hud::CeParticipation\", :permission => :can_edit_project_details }), :CE_ASSESSMENT => ({ :owner_class => \"Hmis::Hud::Assessment\", :permission => :can_edit_enrollments }), :CE_EVENT => ({ :owner_class => \"Hmis::Hud::Event\", :permission => :can_edit_enrollments }), :CASE_NOTE => ({ :owner_class => \"Hmis::Hud::CustomCaseNote\", :permission => :can_edit_enrollments }), :FILE => ({ :owner_class => \"Hmis::File\", :permission => ([:can_manage_any_client_files, :can_manage_own_client_files]), :authorize => (lambda do\n Hmis::File.authorize_proc.call(entity_base, user)\n end) }), :REFERRAL_REQUEST => ({ :owner_class => \"HmisExternalApis::AcHmis::ReferralRequest\", :permission => :can_manage_incoming_referrals }), :REFERRAL => ({ :owner_class => \"HmisExternalApis::AcHmis::ReferralPosting\", :permission => :can_manage_outgoing_referrals }), :CURRENT_LIVING_SITUATION => ({ :owner_class => \"Hmis::Hud::CurrentLivingSituation\", :permission => :can_edit_enrollments }), :OCCURRENCE_POINT => ({ :owner_class => \"Hmis::Hud::Enrollment\", :permission => :can_edit_enrollments }), :ENROLLMENT => ({ :owner_class => \"Hmis::Hud::Enrollment\", :permission => :can_edit_enrollments }), :NEW_CLIENT_ENROLLMENT => ({ :permission => :can_edit_enrollments, :owner_class => \"Hmis::Hud::Enrollment\" }), :CLIENT_DETAIL => ({ :owner_class => \"Hmis::Hud::Client\", :permission => :can_edit_clients }), :EXTERNAL_FORM => ({ :owner_class => \"HmisExternalApis::ExternalForms::FormSubmission\", :permission => :can_manage_external_form_submissions }) }[role.to_sym][:owner_class].constantize", "render_path": null, @@ -1140,7 +1140,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "lib/rds_sql_server/rds.rb", - "line": 244, + "line": 260, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "SqlServerBootstrapModel.connection.execute(\"if not exists(select * from sys.databases where name = '#{database}')\\n select 0;\\nelse\\n select 1;\\n\")", "render_path": null, @@ -1186,7 +1186,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 14, + "line": 18, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "where(\"include_in_#{period}_period\" => true)", "render_path": null, @@ -1209,7 +1209,7 @@ "check_name": "Redirect", "message": "Possible unprotected redirect", "file": "app/controllers/user_training_controller.rb", - "line": 56, + "line": 75, "link": "https://brakemanscanner.org/docs/warning_types/redirect/", "code": "redirect_to(Talentlms::Facade.new(current_user).course_url(course.config, course.courseid, (clients_url or root_url), logout_talentlms_url), :allow_other_host => true)", "render_path": null, @@ -1282,6 +1282,25 @@ ], "note": "Injection from internal sources" }, + { + "warning_type": "Unmaintained Dependency", + "warning_code": 123, + "fingerprint": "715ee6d743a8af33c7b930d728708ce19c765fb40e2ad9d2b974db04d92dc7d1", + "check_name": "EOLRuby", + "message": "Support for Ruby 3.1.6 ends on 2025-03-31", + "file": ".ruby-version", + "line": 1, + "link": "https://brakemanscanner.org/docs/warning_types/unmaintained_dependency/", + "code": null, + "render_path": null, + "location": null, + "user_input": null, + "confidence": "Weak", + "cwe_id": [ + 1104 + ], + "note": "" + }, { "warning_type": "SQL Injection", "warning_code": 0, @@ -1289,7 +1308,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/collection.rb", - "line": 92, + "line": 98, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "where(\"#{quoted_table_name}.coc_codes ?| #{SqlHelper.quote_sql_array(coc_codes, :type => :varchar)}\")", "render_path": null, @@ -1458,7 +1477,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 37, + "line": 41, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_entering_housing\" => true)", "render_path": null, @@ -1481,7 +1500,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 85, + "line": 89, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_entering_housing\" => true)", "render_path": null, @@ -1527,7 +1546,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 81, + "line": 85, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_reentering\" => true)", "render_path": null, @@ -1550,7 +1569,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 57, + "line": 61, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_in_outflow\" => true)", "render_path": null, @@ -1767,7 +1786,7 @@ "check_name": "ValidationRegex", "message": "Insufficient validation for `postal_code` using `/\\A\\d{5}/`. Use `\\A` and `\\z` as anchors", "file": "drivers/hmis/app/models/hmis/hud/custom_client_address.rb", - "line": 108, + "line": 117, "link": "https://brakemanscanner.org/docs/warning_types/format_validation/", "code": null, "render_path": null, @@ -1835,7 +1854,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/grda_warehouse/data_source.rb", - "line": 88, + "line": 94, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "where(\"#{has_access_to_data_source_through_viewable_entities(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)} OR #{has_access_to_data_source_through_organizations(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)} OR #{has_access_to_data_source_through_projects(user, lambda do\n connection.quote(s)\n end, lambda do\n connection.quote_column_name(s)\n end)}\")", "render_path": null, @@ -1858,7 +1877,7 @@ "check_name": "UnsafeReflection", "message": "Unsafe reflection method `constantize` called on model attribute", "file": "app/controllers/cohorts/clients_controller.rb", - "line": 471, + "line": 473, "link": "https://brakemanscanner.org/docs/warning_types/remote_code_execution/", "code": "GrdaWarehouse::Cohort.available_columns.map(&:class).map(&:name).select do\n (m == params.require(:field))\n end.first.constantize", "render_path": null, @@ -1881,7 +1900,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 33, + "line": 37, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "where(\"#{period}_period_spm_leaver\" => true)", "render_path": null, @@ -1927,7 +1946,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/bi/view_maintainer.rb", - "line": 410, + "line": 414, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "GrdaWarehouseBase.connection.execute(\"DO $$\\nBEGIN\\n CREATE ROLE #{role} WITH NOLOGIN;\\n EXCEPTION WHEN DUPLICATE_OBJECT THEN\\n RAISE NOTICE 'not creating role #{role} -- it already exists';\\nEND\\n$$;\\n\")", "render_path": null, @@ -2007,7 +2026,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_measurement/app/models/performance_measurement/equity_analysis/data.rb", - "line": 211, + "line": 217, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "scope.where(\"#{period}_age\" => (age_params.map do\n Filters::FilterBase.age_range(d)\n end))", "render_path": null, @@ -2030,7 +2049,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/homeless_summary_report/app/models/homeless_summary_report/report.rb", - "line": 881, + "line": 899, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "clients.send(variant).send(\"spm_#{field}\").average(\"spm_#{field}\")", "render_path": null, @@ -2076,7 +2095,7 @@ "check_name": "Execute", "message": "Possible command injection", "file": "app/jobs/worker_status.rb", - "line": 72, + "line": 67, "link": "https://brakemanscanner.org/docs/warning_types/command_injection/", "code": "`curl -k https://#{ENV[\"FQDN\"]}/system_status/details`", "render_path": null, @@ -2132,7 +2151,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 89, + "line": 93, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_inactive\" => true)", "render_path": null, @@ -2155,7 +2174,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 14, + "line": 18, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "where(\"include_in_#{period}_period\" => true)", "render_path": null, @@ -2257,7 +2276,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 57, + "line": 61, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_in_outflow\" => true)", "render_path": null, @@ -2303,7 +2322,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 29, + "line": 33, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_caper_leaver\" => true)", "render_path": null, @@ -2326,7 +2345,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 33, + "line": 37, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "where(\"#{period}_period_spm_leaver\" => true)", "render_path": null, @@ -2372,7 +2391,7 @@ "check_name": "MassAssignment", "message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys", "file": "app/controllers/application_controller.rb", - "line": 71, + "line": 91, "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", "code": "params.permit!", "render_path": null, @@ -2521,7 +2540,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "lib/rds_sql_server/rds.rb", - "line": 258, + "line": 274, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "SqlServerBootstrapModel.connection.execute(\"if not exists(select * from sys.databases where name = '#{database}')\\n create database #{database}\\n\")", "render_path": null, @@ -2590,7 +2609,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 81, + "line": 85, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_reentering\" => true)", "render_path": null, @@ -2613,7 +2632,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/bi/view_maintainer.rb", - "line": 419, + "line": 427, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "GrdaWarehouseBase.connection.execute(\"GRANT SELECT ON #{name} TO #{\"bi\"}\")", "render_path": null, @@ -2774,7 +2793,7 @@ "check_name": "SQL", "message": "Possible SQL injection", "file": "drivers/performance_metrics/app/models/performance_metrics/client.rb", - "line": 85, + "line": 89, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "served(period).where(\"#{period}_period_entering_housing\" => true)", "render_path": null, @@ -2860,6 +2879,6 @@ "note": "" } ], - "updated": "2024-10-29 12:24:27 +0000", + "updated": "2025-02-03 21:06:52 +0000", "brakeman_version": "6.2.1" } From f74fa4619800091c3c476aa7d562e3a69f5b6edf Mon Sep 17 00:00:00 2001 From: Dave G <149399758+dtgreiner@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:32:02 -0500 Subject: [PATCH 04/10] hotfix for checkgin if spm enrollment has valid project type (#5103) --- .../app/models/homeless_summary_report/report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/homeless_summary_report/app/models/homeless_summary_report/report.rb b/drivers/homeless_summary_report/app/models/homeless_summary_report/report.rb index 16e41c62c04..8d3d90696ef 100644 --- a/drivers/homeless_summary_report/app/models/homeless_summary_report/report.rb +++ b/drivers/homeless_summary_report/app/models/homeless_summary_report/report.rb @@ -508,7 +508,7 @@ def title_for(household_category, demographic_category) # this must be for a PH project, exluding RRH valid_project_types = HudUtility2024.permanent_housing_project_types - [HudUtility2024.project_type_number('PH - RRH')] - valid_project = valid_project_types.include?(spm_member.enrollment.project.project_type) + valid_project = valid_project_types.include?(spm_member.enrollment&.project&.project_type) valid_project && valid_move_in && valid_exit end From e284a34113575449c1b772b99a930dc6a09c5d71 Mon Sep 17 00:00:00 2001 From: Gig Date: Wed, 5 Feb 2025 07:54:44 -0500 Subject: [PATCH 05/10] Change handling of legacy occurrence point data (#5022) --- .../hmis_schema/occurrence_point_form.rb | 2 +- .../form/occurrence_point_form_collection.rb | 111 ++++++++++++++++++ .../hmis/app/models/hmis/hud/enrollment.rb | 45 +------ drivers/hmis/app/models/hmis/hud/project.rb | 12 +- .../spec/factories/hmis/form/definitions.rb | 2 +- .../spec/models/hmis/hud/enrollment_spec.rb | 86 ++++++++------ .../hmis/spec/models/hmis/hud/project_spec.rb | 13 +- 7 files changed, 178 insertions(+), 93 deletions(-) create mode 100644 drivers/hmis/app/models/hmis/form/occurrence_point_form_collection.rb diff --git a/drivers/hmis/app/graphql/types/hmis_schema/occurrence_point_form.rb b/drivers/hmis/app/graphql/types/hmis_schema/occurrence_point_form.rb index 6adad4a84a5..a937ff9ece3 100644 --- a/drivers/hmis/app/graphql/types/hmis_schema/occurrence_point_form.rb +++ b/drivers/hmis/app/graphql/types/hmis_schema/occurrence_point_form.rb @@ -14,7 +14,7 @@ class HmisSchema::OccurrencePointForm < Types::BaseObject # Form used for Viewing/Creating/Editing records field :definition, Types::Forms::FormDefinition, null: false, extras: [:parent] - # object is an OpenStruct, see Hmis::Hud::Enrollment occurrence_point_forms + # object is an OpenStruct, see Hmis::Form::OccurrencePointFormCollection def id(parent:) # Include project id (if present) so that instance is not cached for use across projects. diff --git a/drivers/hmis/app/models/hmis/form/occurrence_point_form_collection.rb b/drivers/hmis/app/models/hmis/form/occurrence_point_form_collection.rb new file mode 100644 index 00000000000..0a897425559 --- /dev/null +++ b/drivers/hmis/app/models/hmis/form/occurrence_point_form_collection.rb @@ -0,0 +1,111 @@ +### +# Copyright 2016 - 2025 Green River Data Analysis, LLC +# +# License detail: https://github.com/greenriver/hmis-warehouse/blob/production/LICENSE.md +### + +### +# Hmis::Form::OccurrencePointFormCollection +# +# This class is responsible for determining which Occurrence Point forms to display on a given Enrollment in HMIS. +# The Occurrence Point forms appear in the "Enrollment Details" card on the HMIS Enrollment dashboard. +# +# These forms collect data elements onto an Enrollment "at occurrence" (a.k.a. when they occur), +# as opposed to data elements that are collected at a specific point in time (e.g. at intake, exit). +### +class Hmis::Form::OccurrencePointFormCollection + # Struct that backs Types::HmisSchema::OccurrencePointForm + OccurrencePointForm = Struct.new(:definition, :legacy, :data_collected_about, keyword_init: true) + private_constant :OccurrencePointForm + + # Occurrence Point forms to display on the Enrollment, including legacy forms to show existing data + def for_enrollment(enrollment) + structs = active_for_enrollment(enrollment) + structs += legacy_for_enrollment(enrollment, active_forms: structs) + structs + end + + # Occurrence Point forms that are enabled in the Project. This is only used for purposes of displaying Project configuration. + def for_project(project) + occurrence_point_definition_scope.map do |definition| + # Choose the most specific Instance that enables this FormDefinition for this Project + best_instance = definition.instances.active.order(updated_at: :desc).detect_best_instance_for_project(project: project) + next unless best_instance + + create_form_struct( + definition: definition, + data_collected_about: best_instance.data_collected_about, + legacy: false, # not legacy, because there is an active Form Instance enabling it + ) + end.compact + end + + private + + # Occurrence Point forms that are enabled for this Enrollment via an active form instance + def active_for_enrollment(enrollment) + occurrence_point_definition_scope.map do |definition| + # Choose the most specific Instance that enables this FormDefinition for this Enrollment + best_instance = definition.instances.active.order(updated_at: :desc).detect_best_instance_for_enrollment(enrollment: enrollment) + # If there was no active instance, that means this Occurrence Point form is not enabled. Skip it. + next unless best_instance + + create_form_struct( + definition: definition, + data_collected_about: best_instance.data_collected_about, + legacy: false, # not legacy, because there is an active Form Instance enabling it + ) + end.compact + end + + # Default Occurrence Point forms that collect HUD fields. The system should already enforce that + # these forms are enabled for the appropriate projects (e.g. Move-in Date collected on HoH in PH). + # This code ensures that for contexts when the form ISN'T enabled (e.g. Move-in Date on a Child), + # AND the Enrollment has a value for the primary field it collects (e.g. 'MoveInDate'), we still show the value and the form. + # This allows users to see the full set of HUD occurrence point data elements, and do data correction. + HUD_DEFAULT_FORMS = [ + # Note: form_identifier matches the filename of the form, e.g. ../default/occurrence_point_forms/move_in_date.json + { form_identifier: :move_in_date, field_name: :move_in_date }, + { form_identifier: :date_of_engagement, field_name: :date_of_engagement }, + { form_identifier: :path_status, field_name: :date_of_path_status }, + ].freeze + + def legacy_for_enrollment(enrollment, active_forms:) + # Add legacy forms to ensure that HUD Data Elements are not hidden. + # In the event that an Enrollment has a MoveInDate, for example, but there is no active form that collects it, + # we still need to show it so that user can see the data and perform data correction. + HUD_DEFAULT_FORMS.map do |config| + form_identifier, field_name = config.values_at(:form_identifier, :field_name) + # this enrollment does not have this field (e.g. MoveInDate), skip + next unless enrollment.send(field_name).present? + # this field is already collected by an active enable form, skip + next if active_forms.find { |s| collects_enrollment_field?(s.definition, field_name) } + + definition = occurrence_point_definition_scope.find { |fd| fd.identifier == form_identifier.to_s && fd.managed_in_version_control? } + raise "Unexpected: #{field_name} present, but default form '#{form_identifier}' not found" unless definition + + create_form_struct(definition: definition, legacy: true) + end.compact + end + + def occurrence_point_definition_scope + @occurrence_point_definition_scope ||= Hmis::Form::Definition.with_role(:OCCURRENCE_POINT).published + end + + def create_form_struct(definition:, legacy:, data_collected_about: nil) + OccurrencePointForm.new( + definition: definition, + legacy: legacy, + data_collected_about: data_collected_about || 'ALL_CLIENTS', + ) + end + + # Check if the given FormDefinition collects the given field from the Enrollment. + # This is a bit hacky (transforming fieldname to graphql casing) but it works for the known fields (Move-in date, DOE, PATH). + def collects_enrollment_field?(definition, field_name) + normalized_field_name = field_name.to_s.camelize(:lower) + definition.link_id_item_hash.values.any? do |item| + item.mapping&.record_type == 'ENROLLMENT' && item.mapping&.field_name == normalized_field_name + end + end +end diff --git a/drivers/hmis/app/models/hmis/hud/enrollment.rb b/drivers/hmis/app/models/hmis/hud/enrollment.rb index bdd718e74e1..6083bd3f4f7 100644 --- a/drivers/hmis/app/models/hmis/hud/enrollment.rb +++ b/drivers/hmis/app/models/hmis/hud/enrollment.rb @@ -339,49 +339,10 @@ def data_collection_features end.compact end + # Occurrence Point Forms that are enabled for this Enrollment. + # Returns array of OpenStructs, which are resolved by the HmisSchema::OccurrencePointForm GQL type. def occurrence_point_forms - # Get definitions for Occurrence Point forms, including inactive/retired (but excluding drafts) - definitions = Hmis::Form::Definition.with_role(:OCCURRENCE_POINT).published_or_retired.latest_versions - # Get cdeds that this enrollment has CDE record(s) for. Do this in advance so we don't make extra trips to db - cdeds_this_enrollment_has = custom_data_element_definitions.pluck(:key).to_set - - definitions.map do |definition| - # Choose the most specific instance for this enrollment - best_instance = definition.instances.active.detect_best_instance_for_enrollment(enrollment: self) - - # Check for legacy data. Skip the calculation if there is a current instance - has_legacy_data = best_instance ? false : legacy_occurrence_point_data?(definition, cdeds_this_enrollment_has) - - next unless best_instance || has_legacy_data - - OpenStruct.new( - legacy: has_legacy_data && !best_instance, - definition: definition, - data_collected_about: best_instance&.data_collected_about || 'ALL_CLIENTS', - ) - end.compact - end - - private def legacy_occurrence_point_data?(definition, cdeds_this_enrollment_has) - definition.walk_definition_nodes(as_open_struct: true) do |item| - next unless item.mapping.present? - - record_type = item.mapping&.record_type - field_name = item.mapping&.field_name&.underscore - custom_field_key = item.mapping&.custom_field_key - - next unless record_type == 'ENROLLMENT' || custom_field_key - - if record_type && field_name - # Example: if this item collects `move_in_date` and the Enrollment has a Move-in Date value, then we want to show this form on the Enrollment Dashboard (even though it isn't "enabled" via an instance) - return true if respond_to?(field_name) && send(field_name).present? - elsif custom_field_key - # For simplicity, for now, just look for CDEDs where the owner is this Enrollment - return true if cdeds_this_enrollment_has.include?(custom_field_key) - end - end - - false + Hmis::Form::OccurrencePointFormCollection.new.for_enrollment(self) end def save_new_enrollment! diff --git a/drivers/hmis/app/models/hmis/hud/project.rb b/drivers/hmis/app/models/hmis/hud/project.rb index 8b9f730e305..8427cd22944 100644 --- a/drivers/hmis/app/models/hmis/hud/project.rb +++ b/drivers/hmis/app/models/hmis/hud/project.rb @@ -295,17 +295,7 @@ def available_service_types # Occurrence Point Form Instances that are enabled for this project (e.g. Move In Date form) def occurrence_point_form_instances - # All instances for Occurrence Point forms - base_scope = Hmis::Form::Instance.with_role(:OCCURRENCE_POINT).active.published - - # All possible form identifiers used for Occurrence Point collection - occurrence_point_identifiers = base_scope.pluck(:definition_identifier).uniq - - # Choose the most specific instance for each definition identifier - occurrence_point_identifiers.map do |identifier| - scope = base_scope.where(definition_identifier: identifier).order(updated_at: :desc) - scope.detect_best_instance_for_project(project: self) - end.compact + Hmis::Form::OccurrencePointFormCollection.new.for_project(self) end def uniq_coc_codes diff --git a/drivers/hmis/spec/factories/hmis/form/definitions.rb b/drivers/hmis/spec/factories/hmis/form/definitions.rb index 5204f962609..69c9b68d6a7 100644 --- a/drivers/hmis/spec/factories/hmis/form/definitions.rb +++ b/drivers/hmis/spec/factories/hmis/form/definitions.rb @@ -852,7 +852,7 @@ end factory :occurrence_point_form, parent: :hmis_form_definition do - identifier { 'move_in_date' } + identifier { 'move_in_date_form' } role { :OCCURRENCE_POINT } definition do JSON.parse(<<~JSON) diff --git a/drivers/hmis/spec/models/hmis/hud/enrollment_spec.rb b/drivers/hmis/spec/models/hmis/hud/enrollment_spec.rb index 2e25290811d..43eb7a92d0a 100644 --- a/drivers/hmis/spec/models/hmis/hud/enrollment_spec.rb +++ b/drivers/hmis/spec/models/hmis/hud/enrollment_spec.rb @@ -273,14 +273,14 @@ let(:legacy_expected_struct) do have_attributes( legacy: true, - definition: definition, + definition: have_attributes(identifier: 'move_in_date'), # default form seeded by JsonForms.seed_all data_collected_about: 'ALL_CLIENTS', ) end before(:all) do - Hmis::Form::Definition.delete_all - Hmis::Form::Instance.delete_all + # seed default FormDefinitions so that the default move_in_date form is present + ::HmisUtil::JsonForms.seed_all end it 'does not return the form when no instance exists' do @@ -297,7 +297,7 @@ expect(hoh_enrollment.occurrence_point_forms).to be_empty end - context 'when there is no instance, but there is legacy data' do + context 'when there is no instance, but Enrollment has a MoveInDate value' do let!(:spouse_enrollment) do create( :hmis_hud_enrollment, @@ -309,7 +309,7 @@ ) end - it 'does return the form' do + it 'does return the default move_in_date form' do expect(spouse_enrollment.occurrence_point_forms).to contain_exactly(legacy_expected_struct) end @@ -319,7 +319,7 @@ let!(:instance3) { create(:hmis_form_instance, role: role, project_type: 4, active: true, definition: definition) } let!(:inactive_instance) { create(:hmis_form_instance, role: role, project_type: 6, active: false, definition: definition) } - it 'returns the form, with no duplicates' do + it 'returns the default move_in_date form, with no duplicates' do expect(spouse_enrollment.occurrence_point_forms).to contain_exactly(legacy_expected_struct) end end @@ -327,7 +327,7 @@ context 'when a draft version of the form does not collect the same data' do let!(:draft_definition) { create(:occurrence_point_form, version: 2, status: :draft) } - it 'returns the form' do + it 'returns the default move_in_date form' do expect(spouse_enrollment.occurrence_point_forms).to contain_exactly(legacy_expected_struct) end end @@ -351,6 +351,7 @@ definition: definition, data_collected_about: 'ALL_CLIENTS', ) + # binding.pry expect(hoh_enrollment.occurrence_point_forms).to contain_exactly(expected) expect(spouse_enrollment.occurrence_point_forms).to contain_exactly(expected) end @@ -382,32 +383,7 @@ ) end - it 'returns the form for non-HoH' do - expect(spouse_enrollment.occurrence_point_forms).to contain_exactly(legacy_expected_struct) - end - end - - context 'when legacy data exists on a CDED' do - let!(:definition_json) do - { - 'item': [ - { - 'text': 'Foo data element', - 'type': 'STRING', - 'link_id': 'foo', - 'mapping': { - 'custom_field_key': 'foo', - 'record_type': 'ENROLLMENT', - }, - }, - ], - } - end - let!(:definition) { create :occurrence_point_form, definition: definition_json } - let!(:cded) { create :hmis_custom_data_element_definition, key: 'foo', data_source: ds1, owner_type: 'Hmis::Hud::Enrollment', repeats: false } - let!(:cde) { create :hmis_custom_data_element, data_element_definition: cded, owner: spouse_enrollment, data_source: ds1, value_string: 'bar' } - - it 'returns the form for non-HoH' do + it 'returns the default form for non-HoH' do expect(spouse_enrollment.occurrence_point_forms).to contain_exactly(legacy_expected_struct) end end @@ -428,5 +404,49 @@ end end end + + context 'PATH Status form' do + let(:legacy_expected_struct) do + have_attributes( + legacy: true, + definition: have_attributes(identifier: 'path_status'), # default form seeded by JsonForms.seed_all + data_collected_about: 'ALL_CLIENTS', + ) + end + + context 'when DateOfPATHStatus does not exist' do + it 'does not return PATH status form' do + expect(hoh_enrollment.occurrence_point_forms).to be_empty + end + end + context 'when DateOfPATHStatus exists' do + before(:each) { hoh_enrollment.update!(date_of_path_status: 3.weeks.ago) } + it 'returns the default PATH status form' do + expect(hoh_enrollment.occurrence_point_forms).to contain_exactly(legacy_expected_struct) + end + end + end + + context 'Date of Engagement form' do + let(:legacy_expected_struct) do + have_attributes( + legacy: true, + definition: have_attributes(identifier: 'date_of_engagement'), # default form seeded by JsonForms.seed_all + data_collected_about: 'ALL_CLIENTS', + ) + end + + context 'when DateOfEngagement does not exist' do + it 'does not return Date of engagement form' do + expect(hoh_enrollment.occurrence_point_forms).to be_empty + end + end + context 'when DateOfEngagement exists' do + before(:each) { hoh_enrollment.update!(date_of_engagement: 3.weeks.ago) } + it 'returns the default Date of engagement form' do + expect(hoh_enrollment.occurrence_point_forms).to contain_exactly(legacy_expected_struct) + end + end + end end end diff --git a/drivers/hmis/spec/models/hmis/hud/project_spec.rb b/drivers/hmis/spec/models/hmis/hud/project_spec.rb index 0dbfe7b02cd..80138635e75 100644 --- a/drivers/hmis/spec/models/hmis/hud/project_spec.rb +++ b/drivers/hmis/spec/models/hmis/hud/project_spec.rb @@ -209,13 +209,16 @@ def selected_instances end it 'returns most specific instance per definition identifier' do - mid_ptype = create(:hmis_form_instance, role: role, entity: nil, project_type: 13, definition_identifier: 'move_in_date') - mid_project = create(:hmis_form_instance, role: role, entity: project, definition_identifier: mid_ptype.definition_identifier) + mid_ptype = create(:hmis_form_instance, role: role, entity: nil, project_type: 13, definition_identifier: 'move_in_date', data_collected_about: 'HOH') + mid_project = create(:hmis_form_instance, role: role, entity: project, definition_identifier: mid_ptype.definition_identifier, data_collected_about: 'HOH_AND_ADULTS') - doe_default = create(:hmis_form_instance, role: role, entity: nil, definition_identifier: 'date_of_engagement') - doe_org = create(:hmis_form_instance, role: role, entity: project.organization, definition_identifier: doe_default.definition_identifier) + doe_default = create(:hmis_form_instance, role: role, entity: nil, definition_identifier: 'date_of_engagement', data_collected_about: 'ALL_CLIENTS') + doe_org = create(:hmis_form_instance, role: role, entity: project.organization, definition_identifier: doe_default.definition_identifier, data_collected_about: 'HOH') - expect(selected_instances).to contain_exactly(mid_project, doe_org) + expect(selected_instances).to contain_exactly( + have_attributes(definition: mid_project.definition, data_collected_about: mid_project.data_collected_about), + have_attributes(definition: doe_default.definition, data_collected_about: doe_org.data_collected_about), + ) end it 'does not return draft forms, even for active instances' do From b0eaa4a85ce80e720f6aa22692ec8ae8b1ff458f Mon Sep 17 00:00:00 2001 From: Gig Date: Wed, 5 Feb 2025 08:11:37 -0500 Subject: [PATCH 06/10] Add picklist `ADMIN_AVAILABLE_UNITS_FOR_ENROLLMENT` for AC unit changes (#5111) --- drivers/hmis/app/graphql/schema.graphql | 9 +++- .../types/forms/enums/pick_list_type.rb | 3 +- .../graphql/types/forms/pick_list_option.rb | 26 ++++++++-- .../hmis/app/models/hmis/hud/enrollment.rb | 1 + .../hmis/spec/requests/hmis/pick_list_spec.rb | 50 +++++++++++++------ 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/drivers/hmis/app/graphql/schema.graphql b/drivers/hmis/app/graphql/schema.graphql index 74cd01eb2b8..2e367a03449 100644 --- a/drivers/hmis/app/graphql/schema.graphql +++ b/drivers/hmis/app/graphql/schema.graphql @@ -6656,6 +6656,12 @@ type PickListOption { } enum PickListType { + """ + Units available for the given Enrollment at the given project. Includes all + available units at project even if they have a different type from what the + household is currently occupying. + """ + ADMIN_AVAILABLE_UNITS_FOR_ENROLLMENT ALL_SERVICE_CATEGORIES ALL_SERVICE_TYPES @@ -6680,7 +6686,8 @@ enum PickListType { AVAILABLE_SERVICE_TYPES """ - Units available for the given household at the given project + Units available for the given Enrollment at the given project. List is limited + to units with the same unit type currently occupied by the household, if any. """ AVAILABLE_UNITS_FOR_ENROLLMENT diff --git a/drivers/hmis/app/graphql/types/forms/enums/pick_list_type.rb b/drivers/hmis/app/graphql/types/forms/enums/pick_list_type.rb index bc3dda05be0..88c4d699a5b 100644 --- a/drivers/hmis/app/graphql/types/forms/enums/pick_list_type.rb +++ b/drivers/hmis/app/graphql/types/forms/enums/pick_list_type.rb @@ -27,7 +27,8 @@ class Forms::Enums::PickListType < Types::BaseEnum value 'ALL_UNIT_TYPES', 'All unit types.' value 'POSSIBLE_UNIT_TYPES_FOR_PROJECT', 'Unit types that are eligible to be added to project' value 'AVAILABLE_UNIT_TYPES', 'Unit types that have unoccupied units in the specified project' - value 'AVAILABLE_UNITS_FOR_ENROLLMENT', 'Units available for the given household at the given project' + value 'AVAILABLE_UNITS_FOR_ENROLLMENT', 'Units available for the given Enrollment at the given project. List is limited to units with the same unit type currently occupied by the household, if any.' + value 'ADMIN_AVAILABLE_UNITS_FOR_ENROLLMENT', 'Units available for the given Enrollment at the given project. Includes all available units at project even if they have a different type from what the household is currently occupying.' value 'ALL_SERVICE_TYPES' value 'ALL_SERVICE_CATEGORIES' value 'CUSTOM_SERVICE_CATEGORIES' diff --git a/drivers/hmis/app/graphql/types/forms/pick_list_option.rb b/drivers/hmis/app/graphql/types/forms/pick_list_option.rb index 44fbd670c2a..9cff15d2bd3 100644 --- a/drivers/hmis/app/graphql/types/forms/pick_list_option.rb +++ b/drivers/hmis/app/graphql/types/forms/pick_list_option.rb @@ -70,6 +70,8 @@ def self.options_for_type(pick_list_type, user:, project_id: nil, client_id: nil available_unit_types_for_project(project) when 'AVAILABLE_UNITS_FOR_ENROLLMENT' available_units_for_enrollment(project, household_id: household_id) + when 'ADMIN_AVAILABLE_UNITS_FOR_ENROLLMENT' + admin_available_units_for_enrollment(project, household_id: household_id) when 'OPEN_HOH_ENROLLMENTS_FOR_PROJECT' open_hoh_enrollments_for_project(project, user: user) when 'ENROLLMENTS_FOR_CLIENT' @@ -474,7 +476,7 @@ def self.enrollments_for_client(client, user:) end end - def self.available_units_for_enrollment(project, household_id: nil) + def self.admin_available_units_for_enrollment(project, household_id: nil) return [] unless project # Eligible units are unoccupied units, PLUS units occupied by household members @@ -487,10 +489,8 @@ def self.available_units_for_enrollment(project, household_id: nil) [] end - unit_types_assigned_to_household = Hmis::Unit.where(id: hh_units).pluck(:unit_type_id).compact.uniq eligible_units = Hmis::Unit.where(id: unoccupied_units + hh_units) - # If some household members are assigned to units with unit types, then list should be limited to units of the same type. - eligible_units = eligible_units.where(unit_type_id: unit_types_assigned_to_household) if unit_types_assigned_to_household.any? + eligible_units.preload(:unit_type). order(:unit_type_id, :id). map do |unit| @@ -504,6 +504,24 @@ def self.available_units_for_enrollment(project, household_id: nil) end end + def self.available_units_for_enrollment(project, household_id: nil) + return [] unless project + + # use picklist that includes all available units including units of other types + picklist = admin_available_units_for_enrollment(project, household_id: household_id) + return picklist unless household_id # no household, so no need to filter unit types + + # drop units that have different types + hh_unit_type_ids = project.enrollments.where(household_id: household_id).map(&:current_unit_type).compact.map(&:id).uniq + return picklist if hh_unit_type_ids.empty? # household doesn't have a unit type, so no need for further filtering + + # if the household has a unit type, exclude units that don't match + allowed_unit_type_unit_ids = project.units.where(unit_type_id: hh_unit_type_ids).pluck(:id).to_set + picklist.filter do |option| + option[:code].in?(allowed_unit_type_unit_ids) + end + end + def self.assessment_names_for_project(project) # It's a little odd to combine the "roles" (eg INTAKE) with the identifiers (eg housing_needs_assessment), but # we need to do that in order to get the desired behavior. The "Intake" option should show all Intakes, diff --git a/drivers/hmis/app/models/hmis/hud/enrollment.rb b/drivers/hmis/app/models/hmis/hud/enrollment.rb index 6083bd3f4f7..211efd07a32 100644 --- a/drivers/hmis/app/models/hmis/hud/enrollment.rb +++ b/drivers/hmis/app/models/hmis/hud/enrollment.rb @@ -82,6 +82,7 @@ class Hmis::Hud::Enrollment < Hmis::Hud::Base has_many :unit_occupancies, class_name: 'Hmis::UnitOccupancy', inverse_of: :enrollment, dependent: :destroy has_one :active_unit_occupancy, -> { active }, class_name: 'Hmis::UnitOccupancy', inverse_of: :enrollment has_one :current_unit, through: :active_unit_occupancy, class_name: 'Hmis::Unit', source: :unit + has_one :current_unit_type, through: :current_unit, class_name: 'Hmis::UnitType', source: :unit_type # Cached chronically homeless at entry has_one :ch_enrollment, class_name: 'Hmis::ChEnrollment', dependent: :destroy diff --git a/drivers/hmis/spec/requests/hmis/pick_list_spec.rb b/drivers/hmis/spec/requests/hmis/pick_list_spec.rb index 3c327787d04..9e5a55ba2c6 100644 --- a/drivers/hmis/spec/requests/hmis/pick_list_spec.rb +++ b/drivers/hmis/spec/requests/hmis/pick_list_spec.rb @@ -308,36 +308,56 @@ def picklist_option_codes(project) end end - describe 'AVAILABLE_UNITS_FOR_ENROLLMENT' do + describe 'unit picklists' do let!(:e1) { create :hmis_hud_enrollment, data_source: ds1, project: p1, client: c1 } - let!(:un1) { create :hmis_unit, project: p1 } - let!(:un2) { create :hmis_unit, project: p1 } - let!(:un3) { create :hmis_unit } # in another project + let!(:br1) { create :hmis_unit_type, description: '1 BR' } + let!(:br2) { create :hmis_unit_type, description: '2 BR' } + + let!(:un1) { create :hmis_unit, project: p1, unit_type: br1 } + let!(:un2) { create :hmis_unit, project: p1, unit_type: br1 } + let!(:un3) { create :hmis_unit, project: p1, unit_type: br2 } + + # cruft: units in other projects + let!(:un4) { create :hmis_unit, unit_type: br1 } + let!(:un5) { create :hmis_unit, unit_type: br2 } + let!(:un6) { create :hmis_unit } # assign e1 to un1 let!(:uo1) { create :hmis_unit_occupancy, unit: un1, enrollment: e1, start_date: 1.week.ago } - def picklist_option_codes(project, household_id = nil) + def picklist_option_codes(project, picklist: 'AVAILABLE_UNITS_FOR_ENROLLMENT', household_id: nil) Types::Forms::PickListOption.options_for_type( - 'AVAILABLE_UNITS_FOR_ENROLLMENT', + picklist, user: hmis_user, project_id: project.id, household_id: household_id, ).map { |opt| opt[:code] } end - it 'resolves available units for project' do - expect(picklist_option_codes(p1)).to contain_exactly(un2.id) - end + context 'AVAILABLE_UNITS_FOR_ENROLLMENT' do + it 'resolves available units for project' do + expect(picklist_option_codes(p1)).to contain_exactly(un2.id, un3.id) + end + + it 'includes units that are currently occupied by the household, plus other units of the same type' do + result = picklist_option_codes(p1, household_id: e1.household_id) + expect(result).to contain_exactly(un1.id, un2.id) + end - it 'includes units that are currently occupied by the household' do - expect(picklist_option_codes(p1, e1.household_id)).to contain_exactly(un1.id, un2.id) + it 'if household unit doesn\'t have a type, includes all available units' do + un1.update!(unit_type: nil) + expect(picklist_option_codes(p1, household_id: e1.household_id)).to contain_exactly(un1.id, un2.id, un3.id) + end end - it 'if household is occupied by a unit that has a type, excludes other unit typoes from list' do - un1.unit_type = create(:hmis_unit_type) - un1.save! - expect(picklist_option_codes(p1, e1.household_id)).to contain_exactly(un1.id) + context 'ADMIN_AVAILABLE_UNITS_FOR_ENROLLMENT' do + it 'resolves available units for project' do + expect(picklist_option_codes(p1)).to contain_exactly(un2.id, un3.id) + end + + it 'includes units with differing unit types' do + expect(picklist_option_codes(p1, picklist: 'ADMIN_AVAILABLE_UNITS_FOR_ENROLLMENT', household_id: e1.household_id)).to contain_exactly(un1.id, un2.id, un3.id) + end end end From 761b8fbadd2b0d743d2113d90e0c28db9007be68 Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 5 Feb 2025 16:10:56 -0500 Subject: [PATCH 07/10] Expose RRH Sub Types and limit HMIS DQ Tool (#5112) * Differentiate RRH with housing from those without * Expose RRH Sub Type in various locations; Limit HMIS DQ Tool checks for overlapping data to RRH projects that provide housing --- app/controllers/api/projects_controller.rb | 5 ++-- app/models/grda_warehouse/hud/project.rb | 12 +++++++++ app/views/clients/_enrollment_table.haml | 4 +-- app/views/data_sources/_project.haml | 4 +-- app/views/projects/show.haml | 2 +- .../grda_warehouse/hud/client_extension.rb | 1 + .../models/hmis_data_quality_tool/client.rb | 4 +-- lib/util/hud_utility_2024.rb | 25 +++++++++++++++++++ 8 files changed, 48 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index b4b1ab18ba3..42a3af06a84 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -24,14 +24,15 @@ def index :id, :ProjectName, # OK to use non-confidentialized name because list is filtered by confidentiality in project_scope :ProjectType, + :RRHSubType, o_t[:OrganizationName], o_t[:id], ds_t[:short_name], - ).each do |id, p_name, type, o_name, o_id, ds_name| + ).each do |id, p_name, type, rrh_sub_type, o_name, o_id, ds_name| o_name_at_ds = "#{o_name} at #{ds_name}" @data[[o_id, o_name_at_ds]] ||= [] - p_name += " (#{HudUtility2024.project_type_brief(type)})" if HudUtility2024.project_type_brief(type).present? + p_name += " (#{HudUtility2024.brief_project_type_with_sub_type(type, rrh_sub_type)})" if HudUtility2024.brief_project_type_with_sub_type(type).present? @data[[o_id, o_name_at_ds]] << [ p_name, id, diff --git a/app/models/grda_warehouse/hud/project.rb b/app/models/grda_warehouse/hud/project.rb index 9e745400ba6..51a6c262008 100644 --- a/app/models/grda_warehouse/hud/project.rb +++ b/app/models/grda_warehouse/hud/project.rb @@ -590,6 +590,18 @@ def rrh? project_type_to_use.in?(HudUtility2024.performance_reporting[:rrh]) end + def rrh_sso_only? + rrh? && self.RRHSubType == rrh_sso_sub_type_code + end + + def rrh_with_housing? + rrh? && self.RRHSubType != rrh_sso_sub_type_code + end + + def rrh_sso_sub_type_code + HudUtility2024.rrh_sub_type('RRH: Services Only', true) + end + def psh? project_type_to_use.in?(HudUtility2024.performance_reporting[:psh]) end diff --git a/app/views/clients/_enrollment_table.haml b/app/views/clients/_enrollment_table.haml index 755b0ace4d6..e16e1635f37 100644 --- a/app/views/clients/_enrollment_table.haml +++ b/app/views/clients/_enrollment_table.haml @@ -50,8 +50,8 @@ %td.nobr .ds.jClickToCopy{class: "ds-color-#{e[:data_source_id]}", data: { toggle: :tooltip, html: 'true', title: ds_tooltip_content(e[:client_source_id], e[:data_source_id]).html_safe }}= ds_short_name_for(e[:client_source_id]) .enrollment__project_type{class: e[:class]} - %span.service-type__program-type{data: {toggle: :tooltip, title: HudUtility2024.project_type(e[:project_type_id])}} - = e[:project_type] + %span.service-type__program-type{data: {toggle: :tooltip, title: HudUtility2024.project_type_with_sub_type(e[:project_type_id], e[:rrh_sub_type])}} + = HudUtility2024.brief_project_type_with_sub_type(e[:project_type_id], e[:rrh_sub_type]) - if e[:confidential_project] && can_view_confidential_project_names? .confidential_project{data: {toggle: :tooltip, title: GrdaWarehouse::Hud::Project.confidential_project_name}} CP diff --git a/app/views/data_sources/_project.haml b/app/views/data_sources/_project.haml index 9b6da6616cc..741059e123b 100644 --- a/app/views/data_sources/_project.haml +++ b/app/views/data_sources/_project.haml @@ -4,8 +4,8 @@ = link_to_if can_view_projects?, project.name(current_user, ignore_confidential_status: can_edit_projects?), project_path(project) %td.d-flex .enrollment__project_type.mr-2{class: "client__service_type_#{project.ProjectType}"} - .service-type__program-type{data: {toggle: :tooltip, title: HudUtility2024.project_type(project.ProjectType)}} - = HudUtility2024.project_type_brief project.ProjectType + .service-type__program-type{data: { toggle: :tooltip, title: HudUtility2024.project_type_with_sub_type(project.project_type, project.rrh_sub_type) }} + = HudUtility2024.brief_project_type_with_sub_type(project.project_type, project.rrh_sub_type) %td.text-center %span{data: {toggle: :tooltip, title: project.confidential_hint}} = checkmark project.confidential diff --git a/app/views/projects/show.haml b/app/views/projects/show.haml index 082e1a11e05..c837d9f1726 100644 --- a/app/views/projects/show.haml +++ b/app/views/projects/show.haml @@ -15,7 +15,7 @@ %tr %th Project Type %td - = HudUtility2024.project_type(@project.ProjectType) + = HudUtility2024.project_type_with_sub_type(@project.project_type, @project.rrh_sub_type) - if @project.active_homeless_status_override.present? %br %em Enrolled clients are actively homeless for CAS and Cohorts diff --git a/drivers/client_access_control/extensions/grda_warehouse/hud/client_extension.rb b/drivers/client_access_control/extensions/grda_warehouse/hud/client_extension.rb index 5eb77aed5e6..a8305a03b7d 100644 --- a/drivers/client_access_control/extensions/grda_warehouse/hud/client_extension.rb +++ b/drivers/client_access_control/extensions/grda_warehouse/hud/client_extension.rb @@ -228,6 +228,7 @@ def enrollments_for_rollup(user:, en_scope: scope, include_confidential_names: f household: household(entry.household_id, entry.enrollment.data_source_id), project_type: ::HudUtility2024.project_type_brief(entry.project_type), project_type_id: entry.project_type, + rrh_sub_type: project.rrh_sub_type, class: "client__service_type_#{entry.project_type}", most_recent_service: most_recent_service, new_episode: new_episode, diff --git a/drivers/hmis_data_quality_tool/app/models/hmis_data_quality_tool/client.rb b/drivers/hmis_data_quality_tool/app/models/hmis_data_quality_tool/client.rb index 503135403fc..2e07009fd11 100644 --- a/drivers/hmis_data_quality_tool/app/models/hmis_data_quality_tool/client.rb +++ b/drivers/hmis_data_quality_tool/app/models/hmis_data_quality_tool/client.rb @@ -241,7 +241,7 @@ def self.overlapping_entry_exit(enrollments:, report:) # check for overlapping PH post-move-in def self.overlapping_post_move_in(enrollments:, report:) involved_enrollments = enrollments.select do |en| - en.project&.ph? + en.project&.ph? && ! en.project&.rrh_sso_only? end return [] if involved_enrollments.blank? || involved_enrollments.count == 1 @@ -288,7 +288,7 @@ def self.overlapping_homeless_post_move_in(enrollments:, report:) # moved-in PH enrollments involved_enrollments = enrollments.select do |en| - en.project&.ph? && en.MoveInDate.present? + en.project&.ph? && ! en.project&.rrh_sso_only? && en.MoveInDate.present? end return [] if involved_enrollments.blank? diff --git a/lib/util/hud_utility_2024.rb b/lib/util/hud_utility_2024.rb index 07af208c69a..ae01a6f5986 100644 --- a/lib/util/hud_utility_2024.rb +++ b/lib/util/hud_utility_2024.rb @@ -86,6 +86,31 @@ def veteran_status(*args) no_yes_reasons_for_missing_data(*args) end + def project_type_with_sub_type(type, sub_type = nil) + [ + project_type(type), + rrh_sub_type(sub_type), + ].compact_blank.join(' — ') + end + + def brief_project_type_with_sub_type(type, sub_type = nil) + [ + project_type_brief(type), + rrh_sub_type_brief(sub_type), + ].compact_blank.join(' — ') + end + + def rrh_sub_types_brief + { + 1 => 'SSO', + 2 => 'Housing', + }.freeze + end + + def rrh_sub_type_brief(id, reverse = false, raise_on_missing: false) + _translate(rrh_sub_types_brief, id, reverse, raise_on_missing: raise_on_missing) + end + def project_type_number(type) # attempt to lookup full name number = project_type(type, true) # reversed From cf61a7b45ae0c610586b26a37d9a55a7d6281923 Mon Sep 17 00:00:00 2001 From: martha Date: Thu, 6 Feb 2025 07:20:48 -0500 Subject: [PATCH 08/10] Update HMIS system tests for new Table Action Column pattern (#5039) --- .../spec/system/hmis/assessment_definitions_spec.rb | 2 +- drivers/hmis/spec/system/hmis/bulk_services_spec.rb | 10 +++++----- .../hmis/spec/system/hmis/intake_assessment_spec.rb | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/drivers/hmis/spec/system/hmis/assessment_definitions_spec.rb b/drivers/hmis/spec/system/hmis/assessment_definitions_spec.rb index ded0012c1d7..6a6a2cb7f6a 100644 --- a/drivers/hmis/spec/system/hmis/assessment_definitions_spec.rb +++ b/drivers/hmis/spec/system/hmis/assessment_definitions_spec.rb @@ -176,7 +176,7 @@ sign_in(hmis_user) visit "/client/#{c1.id}/enrollments/#{e1.id}/assessments" - click_link 'Intake' + click_link 'Finish Intake' end def select_member(client) diff --git a/drivers/hmis/spec/system/hmis/bulk_services_spec.rb b/drivers/hmis/spec/system/hmis/bulk_services_spec.rb index 82fc0082a1c..5d04f1234e6 100644 --- a/drivers/hmis/spec/system/hmis/bulk_services_spec.rb +++ b/drivers/hmis/spec/system/hmis/bulk_services_spec.rb @@ -8,7 +8,7 @@ require_relative '../../requests/hmis/login_and_permissions' require_relative '../../support/hmis_base_setup' -RSpec.feature 'Hmis Form behavior', type: :system do +RSpec.feature 'Bulk Services behavior', type: :system do include_context 'hmis base setup' include_context 'hmis service setup' @@ -54,9 +54,9 @@ # Find the indices of the two columns we want to check header_cells = all('thead th') last_bed_night_date_index = header_cells.find_index { |cell| cell.text == 'Last Bed Night Date' } - assign_bed_night_index = header_cells.find_index { |cell| cell.text == "Assign Bed Night for #{bed_night_date.strftime('%m/%d/%Y')}" } + button_column_index = header_cells.find_index { |cell| cell.text == 'Actions' } expect(last_bed_night_date_index).not_to be_nil - expect(assign_bed_night_index).not_to be_nil + expect(button_column_index).not_to be_nil # Verify that all 3 rows have the expected attributes all('tbody tr').each do |row| @@ -64,8 +64,8 @@ last_bed_night_date = row.all('td')[last_bed_night_date_index].text expect(last_bed_night_date).to match(/#{bed_night_date.strftime('%m/%d/%Y')}/) - # Check the "Assign Bed Night for mm/dd/yyyy" column - assign_button = row.all('td')[assign_bed_night_index].find('button') + # Check the "Actions" column + assign_button = row.all('td')[button_column_index].first('button') expect(assign_button.text).to eq('Assigned') end diff --git a/drivers/hmis/spec/system/hmis/intake_assessment_spec.rb b/drivers/hmis/spec/system/hmis/intake_assessment_spec.rb index 2a6df7ae4cc..dbe9296de7c 100644 --- a/drivers/hmis/spec/system/hmis/intake_assessment_spec.rb +++ b/drivers/hmis/spec/system/hmis/intake_assessment_spec.rb @@ -8,7 +8,7 @@ require_relative '../../requests/hmis/login_and_permissions' require_relative '../../support/hmis_base_setup' -RSpec.feature 'Enrollment/household management', type: :system do +RSpec.feature 'Intake assessment', type: :system do include_context 'hmis base setup' # could parse CAPYBARA_APP_HOST let!(:ds1) { create(:hmis_data_source, hmis: 'localhost') } From 0f2a2cbb0a2d40ca3e76968767ced78b93626798 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 6 Feb 2025 15:06:35 -0500 Subject: [PATCH 09/10] CE Transfer assessments (#4673) * Additional default translations for known assessment keys * Set IdentifiedPathwaysVersionFourTransfer where appropriate * Adjustments for Q5 and sync logic * Test fix * Pathways 2024 adjustments (#5049) * Updated Pathways Tie Breaker Date * Set first date of service * Update language for tie breaker date * Allow CAS Readiness page to be loaded sync for development * Enforce desc sort on CE assessments * Additional housing barriers * Eviction from pathways or transfer * Calculate transfer score from assessment result * Tie breaker for 2024 transfer assessment * WIP: note to adjust family pathways assessment score calculation * Distinction between pathways (individual) and family pathways; pull days homeless from assessment if available; use overall days homeless for assessment score for family pathways * Additional overall calculations for days homeless; preload source client data source; don't try to get phone numbers from HMIS if no HMIS exists --------- Co-authored-by: Dave G <149399758+dtgreiner@users.noreply.github.com> --- app/models/concerns/cas_client_data.rb | 3 +- .../cas_project_client_calculator/boston.rb | 108 +++++++++++++++++- .../cas_project_client_calculator/default.rb | 5 + app/models/grda_warehouse/hud/assessment.rb | 8 ++ .../grda_warehouse/hud/assessment_question.rb | 38 +++++- app/models/grda_warehouse/hud/client.rb | 8 +- .../tasks/push_clients_to_cas.rb | 23 ++-- app/models/translation.rb | 10 ++ app/views/clients/cas_readiness/edit.haml | 10 +- .../clients/rollup/_assessments.html.haml | 2 +- 10 files changed, 185 insertions(+), 30 deletions(-) diff --git a/app/models/concerns/cas_client_data.rb b/app/models/concerns/cas_client_data.rb index 48064e6ca55..967b9716859 100644 --- a/app/models/concerns/cas_client_data.rb +++ b/app/models/concerns/cas_client_data.rb @@ -881,6 +881,7 @@ def cas_assessment_collected_at :additional_homeless_nights_unsheltered, :calculated_homeless_nights_sheltered, :calculated_homeless_nights_unsheltered, - :total_homeless_nights_sheltered + :total_homeless_nights_sheltered, + :psh_required end end diff --git a/app/models/grda_warehouse/cas_project_client_calculator/boston.rb b/app/models/grda_warehouse/cas_project_client_calculator/boston.rb index 4975e2ef4cc..f4e3e156ef6 100644 --- a/app/models/grda_warehouse/cas_project_client_calculator/boston.rb +++ b/app/models/grda_warehouse/cas_project_client_calculator/boston.rb @@ -33,6 +33,10 @@ def value_for_cas_project_client(client:, column:) end end + def handles_days_homeless? + true + end + def unrelated_columns [ :vispdat_score, @@ -61,7 +65,7 @@ def unrelated_columns cas_assessment_collected_at: 'Date the assessment was collected', # note this is really just assessment_collected_at majority_sheltered: 'Most recent current living situation was sheltered', assessment_score_for_cas: 'Days homeless in the past 3 years for pathways, score for transfer assessments', - tie_breaker_date: 'Date pathways was collected, or Financial Assistance End Date for transfer assessments', + tie_breaker_date: 'Date pathways was collected for Pathways 2023, First Date Homeless for Pathways 2024, or Financial Assistance End Date for Transfer Assessments', financial_assistance_end_date: 'Latest Date Eligible for Financial Assistance response from the most recent pathways assessment', assessor_first_name: 'First name of the user who completed the most recent pathways assessment', assessor_last_name: 'Last name of the user who completed the most recent pathways assessment', @@ -73,6 +77,7 @@ def unrelated_columns days_homeless_for_vispdat_prioritization: 'Unused', hiv_positive: 'HIV/AIDS response from the most recent pathways assessment', meth_production_conviction: 'Meth production response from the most recent pathways assessment', + lifetime_sex_offender: 'Registered sex offender (level 1,2,3) - lifetime registration (SORI)', requires_wheelchair_accessibility: 'Does the client need a wheelchair accessible unit response from the most recent pathways assessment', income_maximization_assistance_requested: 'Did the client request income maximization services response from the most recent pathways assessment', sro_ok: 'Is the client ok with an SRO response from the most recent pathways assessment', @@ -94,7 +99,6 @@ def unrelated_columns hiv_positive: 'c_housing_HIV', income_maximization_assistance_requested: 'c_interest_income_max', sro_ok: 'c_singleadult_sro', - evicted: 'c_pathways_barrier_eviction', rrh_desired: 'c_interested_rrh', housing_barrier: 'c_pathways_barriers_yn', }.freeze @@ -143,6 +147,13 @@ def unrelated_columns :calculated_homeless_nights_unsheltered, :total_homeless_nights_sheltered, :total_homeless_nights_unsheltered, + :date_of_first_service, + :psh_required, + :meth_production_conviction, + :lifetime_sex_offender, + :evicted, + :days_homeless, + :hmis_days_homeless_all_time, ] end # memoize :pathways_questions @@ -186,6 +197,23 @@ def most_recent_transfer_assessment_for_destination(client) nil end + private def lifetime_sex_offender(client) + most_recent_pathways_or_transfer(client). + question_matching_requirement('c_transfer_barrier_SORI', '1').present? + end + + private def evicted(client) + evicted = most_recent_pathways_or_transfer(client).question_matching_requirement('c_pathways_barrier_meth', '1').present? + return true if evicted + + evicted = most_recent_pathways_or_transfer(client). + question_matching_requirement('c_transfer_barrier_PHAterm', '1').present? + return true if evicted + + # Otherwise unknown + nil + end + private def service_need(client) need = most_recent_pathways_or_transfer(client).question_matching_requirement('c_pathways_service_indicators', '1').present? return true if need @@ -321,6 +349,18 @@ def days_homeless_in_last_three_years_cached(client) pre_calculated_days end + # Overrides the usual calculation for first date homeless if available + # If the question doesn't exist on the assessment or is empty, use the usual definition + private def date_of_first_service(client) + field_name = 'c_pathways_first_date_homeless' + answer = most_recent_pathways_or_transfer(client). + question_matching_requirement(field_name)&.AssessmentAnswer + + return client.date_of_first_service if answer.blank? + + answer.to_date + end + # If a client has more than 548 self-reported days (combination of sheltered and unsheltered) # and does not have a verification uploaded, count unsheltered days first, then count sheltered days UP TO 548. # If the self reported days are verified, use the provided amounts. @@ -396,12 +436,46 @@ def total_homeless_nights_sheltered(client) question_matching_requirement('c_pathways_nights_sheltered_warehouse_added_total')&.AssessmentAnswer.to_i || 0 end + # all-time days homeless + def days_homeless(client) + overall_nights_homeless(client) + end + + # this seems to be calculated many different ways + def hmis_days_homeless_all_time(client) + overall_nights_homeless(client) + end + + private def overall_nights_homeless(client) + most_recent_pathways_or_transfer(client). + question_matching_requirement('c_new_boston_homeless_nights_total')&.AssessmentAnswer.to_i || + client.days_homeless + end + private def default_shelter_agency_contacts(client) contact_emails = client.client_contacts.shelter_agency_contacts.where.not(email: nil).pluck(:email) contact_emails << client.source_assessments.max_by(&:assessment_date)&.user&.user_email contact_emails.compact.uniq end + # 0 = No PSH + # 1 = PSH required + # 2 = Either + # Default to either if we don't have an answer + # CAS uses `yes`, `no`, `maybe` strings + private def psh_required(client) + value = most_recent_pathways_or_transfer(client). + question_matching_requirement('c_rrh_transfer_needs_subsidized_housing_resource')&.AssessmentAnswer.to_i || 2 + case value + when 0 + 'no' + when 1 + 'yes' + else + 'maybe' + end + end + private def contact_info_for_rrh_assessment(client) client.client_contacts.case_managers.map(&:full_address).join("\n\n") end @@ -419,6 +493,7 @@ def total_homeless_nights_sheltered(client) 1 => 'IdentifiedPathwaysVersionThreePathways', 2 => 'IdentifiedPathwaysVersionThreeTransfer', 3 => 'IdentifiedPathwaysVersionFourPathways', + 4 => 'IdentifiedPathwaysVersionFourTransfer', }[value.to_i] || 'IdentifiedClientAssessment' end @@ -447,21 +522,42 @@ def total_homeless_nights_sheltered(client) question_matching_requirement('c_latest_date_financial_assistance_eligibility_rrh')&.AssessmentAnswer end + # For 2024/2025 score calculations are: + # Individual Pathways Assessment: days homeless in the past 3 years, prefer Pathways answer clamped to 1,096, use + # days in the past three years from the warehouse if not available. + # Family Pathways Assessment: overall days homeless (all time), prefer Pathways answer, use client.days_homeless if + # not available. + # Transfer Assessment: use assessment score. private def assessment_score_for_cas(client) case cas_assessment_name(client) when 'IdentifiedPathwaysVersionThreePathways', 'IdentifiedPathwaysVersionFourPathways' - days_homeless_in_last_three_years_cached(client) - when 'IdentifiedPathwaysVersionThreeTransfer' + if most_recent_pathways_or_transfer(client).family_pathways_2024? + # Family + overall_days_homeless(client) + else + # Individual + days_homeless_in_last_three_years_cached(client) + end + + # all time homeless days (no cap on total, but self-report limited to 548 if no verification) + # Also need a mechanism to identify family Pathways assessments/AssessmentQuestions + when 'IdentifiedPathwaysVersionThreeTransfer', 'IdentifiedPathwaysVersionFourTransfer' assessment_score(client) end end + # Various tie-breaker dates used for prioritization in CAS when all else is equal + # For Pathways V3, use the date the assessment was collected + # For the V3 Transfer assessment, use the financial assistance end date + # For Pathways V4, use the first date of homelessness private def tie_breaker_date(client) case cas_assessment_name(client) - when 'IdentifiedPathwaysVersionThreePathways', 'IdentifiedPathwaysVersionFourPathways' + when 'IdentifiedPathwaysVersionThreePathways' cas_assessment_collected_at(client) - when 'IdentifiedPathwaysVersionThreeTransfer' + when 'IdentifiedPathwaysVersionThreeTransfer', 'IdentifiedPathwaysVersionFourTransfer' financial_assistance_end_date(client) + when 'IdentifiedPathwaysVersionFourPathways' + date_of_first_service(client) end end diff --git a/app/models/grda_warehouse/cas_project_client_calculator/default.rb b/app/models/grda_warehouse/cas_project_client_calculator/default.rb index ea9c35696bd..df5396bd86a 100644 --- a/app/models/grda_warehouse/cas_project_client_calculator/default.rb +++ b/app/models/grda_warehouse/cas_project_client_calculator/default.rb @@ -15,6 +15,11 @@ def value_for_cas_project_client(client:, column:) client.send(column) end + # Defer calculation of `days_homeless` to PushClientsToCas + def handles_days_homeless? + false + end + def description_for_column(column) custom_descriptions[column].presence || GrdaWarehouse::Hud::Client.cas_columns_data.dig(column, :description) end diff --git a/app/models/grda_warehouse/hud/assessment.rb b/app/models/grda_warehouse/hud/assessment.rb index 6563f737ba3..a126dbe16db 100644 --- a/app/models/grda_warehouse/hud/assessment.rb +++ b/app/models/grda_warehouse/hud/assessment.rb @@ -63,6 +63,10 @@ class Assessment < Base where(AssessmentID: GrdaWarehouse::Hud::AssessmentQuestion.transfer.select(:AssessmentID)) end + scope :family_pathways, -> do + where(AssessmentID: GrdaWarehouse::Hud::AssessmentQuestion.family_pathways.select(:AssessmentID)) + end + def answer(question) assessment_questions.find_by(assessment_question: question.to_s)&.assessment_answer end @@ -105,5 +109,9 @@ def name def pathways? assessment_questions.any?(&:pathways?) end + + def family_pathways_2024? + name == 'Family Pathways 2024' + end end end diff --git a/app/models/grda_warehouse/hud/assessment_question.rb b/app/models/grda_warehouse/hud/assessment_question.rb index 2920dbad971..62b331c06bd 100644 --- a/app/models/grda_warehouse/hud/assessment_question.rb +++ b/app/models/grda_warehouse/hud/assessment_question.rb @@ -47,15 +47,18 @@ class AssessmentQuestion < Base pathways_or_rrh. joins(:lookup). merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: pathways_titles)) - - # Temporary solution until we have the c_housing_assessment_name question in the 2024 pathways assessment - # where(AssessmentQuestion: [:c_housing_assessment_name, :c_pathways_barriers_yn]) end scope :transfer, -> do pathways_or_rrh. joins(:lookup). - merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: 'RRH-PSH Transfer')) + merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: transfer_titles)) + end + + scope :family_pathways, -> do + pathways_or_rrh. + joins(:lookup). + merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: family_pathways_titles)) end # NOTE: you probably want to join/preload :lookup @@ -71,8 +74,6 @@ def default_response_text(answer) end def pathways? - # FIXME: this is temporary until we have a more permanent solution - # self.AssessmentQuestion.to_s == 'c_pathways_barriers_yn' self.AssessmentQuestion.to_s == 'c_housing_assessment_name' && human_readable.in?(self.class.pathways_titles) end @@ -83,6 +84,19 @@ def self.pathways_titles ] end + def self.transfer_titles + [ + 'RRH-PSH Transfer', + 'RRH-PSH Transfer 2024', + ] + end + + def self.family_pathways_titles + [ + 'Family Pathways 2024', + ] + end + private def pathways_question? assessment_question == 'c_housing_assessment_name' end @@ -91,11 +105,17 @@ def assessment_name return 'Pathways 2024' if pathways_2024? return 'Pathways 2021' if pathways_2021? return 'RRH-PSH Transfer' if transfer_2021? + return 'RRH-PSH Transfer 2024' if transfer_2024? + return 'Family Pathways 2024' if family_pathways_2024? # unknown from assessment questions nil end + def family_pathways_2024? + lookup&.response_text == 'Family Pathways 2024' + end + def pathways_2024? return nil unless pathways_question? @@ -113,5 +133,11 @@ def transfer_2021? lookup&.response_text == 'RRH-PSH Transfer' end + + def transfer_2024? + return nil unless pathways_question? + + lookup&.response_text == 'RRH-PSH Transfer 2024' + end end end diff --git a/app/models/grda_warehouse/hud/client.rb b/app/models/grda_warehouse/hud/client.rb index 6eb34424b20..5f14564cecf 100644 --- a/app/models/grda_warehouse/hud/client.rb +++ b/app/models/grda_warehouse/hud/client.rb @@ -1357,7 +1357,7 @@ def email return source_clients.map(&:email).reject(&:blank?).first if destination? # Look for value from OP HMIS - value = most_recent_email_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_email_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? # Look for value from other HMIS integrations value ||= hmis_client_response['Email'] if hmis_client_response.present? value ||= hmis_client.processed_fields['email'] if hmis_client&.processed_fields @@ -1368,7 +1368,7 @@ def home_phone # Fetch the data from the source clients if we are a destination client return source_clients.map(&:home_phone).reject(&:blank?).first if destination? - value = most_recent_home_phone_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_home_phone_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? value ||= hmis_client_response['HomePhone'] if hmis_client_response.present? value end @@ -1377,7 +1377,7 @@ def cell_phone # Fetch the data from the source clients if we are a destination client return source_clients.map(&:cell_phone).reject(&:blank?).first if destination? - value = most_recent_cell_or_other_phone_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_cell_or_other_phone_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? value ||= hmis_client_response['CellPhone'] if hmis_client_response.present? value ||= hmis_client.processed_fields['phone'] if hmis_client&.processed_fields value @@ -1387,7 +1387,7 @@ def work_phone # Fetch the data from the source clients if we are a destination client return source_clients.map(&:work_phone).reject(&:blank?).first if destination? - value = most_recent_work_or_school_phone_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_work_or_school_phone_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? return value if value return unless hmis_client_response.present? diff --git a/app/models/grda_warehouse/tasks/push_clients_to_cas.rb b/app/models/grda_warehouse/tasks/push_clients_to_cas.rb index 217d219991d..8cdde2e6e4b 100644 --- a/app/models/grda_warehouse/tasks/push_clients_to_cas.rb +++ b/app/models/grda_warehouse/tasks/push_clients_to_cas.rb @@ -50,6 +50,7 @@ def sync! source_clients: [ :most_recent_current_living_situation, :most_recent_tc_hat, + :data_source, { most_recent_pathways_or_rrh_assessment: [ :assessment_questions, @@ -76,11 +77,14 @@ def sync! project_client = project_clients[client.id] || CasAccess::ProjectClient.new(data_source_id: data_source.id, id_in_data_source: client.id) project_client.assign_attributes(attributes_for_cas_project_client(client)) - case GrdaWarehouse::Config.get(:cas_days_homeless_source) - when 'days_homeless_plus_overrides' - project_client.days_homeless = client.processed_service_history&.days_homeless_plus_overrides || client.days_homeless - else - project_client.days_homeless = client.days_homeless + # The Boston calculator handles days homeless natively + unless calculator_instance.handles_days_homeless? + case GrdaWarehouse::Config.get(:cas_days_homeless_source) + when 'days_homeless_plus_overrides' + project_client.days_homeless = client.processed_service_history&.days_homeless_plus_overrides || client.days_homeless + else + project_client.days_homeless = client.days_homeless + end end project_client.calculated_last_homeless_night = client.date_of_last_homeless_service @@ -109,8 +113,8 @@ def sync! project_client.needs_update = true to_update << project_client end - to_insert = to_update.select { |c| c.id.blank? } - to_upsert = to_update.select { |c| c.id.present? } + to_insert = to_update.select { |c| c.id.blank? }.uniq + to_upsert = to_update.select { |c| c.id.present? }.uniq CasAccess::ProjectClient.import!(to_upsert, on_duplicate_key_update: update_columns) if to_upsert.present? CasAccess::ProjectClient.import!(update_columns, to_insert) if to_insert.present? @@ -290,12 +294,15 @@ def client_source total_homeless_nights_sheltered: :total_homeless_nights_sheltered, service_need: :service_need, housing_barrier: :housing_barrier, + psh_required: :psh_required, } end private def attributes_for_cas_project_client(client) {}.tap do |options| - project_client_columns.map do |destination, source| + columns = project_client_columns + columns[:days_homeless] = :days_homeless if calculator_instance.handles_days_homeless? + columns.map do |destination, source| # puts "Processing: #{destination} from: #{source}" options[destination] = calculator_instance.value_for_cas_project_client(client: client, column: source) end diff --git a/app/models/translation.rb b/app/models/translation.rb index 2ac71105230..538d922de99 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -1847,6 +1847,16 @@ def self.default_translations 'c_pathways_service_indicators_shelter_restriction' => 'I\'ve faced indefinite restrictions and a history of restrictions from area shelters', 'c_share_permission' => 'Permission to Share Your Information with Partner Agencies', 'c_singleadult_sro' => 'If you are a single adult, would you consider living in a single room occupancy (SRO)?', + 'c_transfer_barrier_meth' => 'Have been convicted or found guilty of producing methamphetamine on subsidized properties', + 'c_transfer_barrier_PHAterm' => 'Have been evicted from a BHA development or have had a BHA voucher terminated within the last three years', + 'c_transfer_barrier_SORI' => 'Registered sex offender (level 1,2,3) - lifetime registration (SORI)', + 'c_rrh_transfer_daily_life_functions' => 'Does client have a physical or mental impairment that substantially limits one or more daily life functions? Daily life functions include: obtaining food/eating, sleeping, physical movement, caring for one’s personal hygiene, and communicating.', + 'c_rrh_transfer_addiction_history' => 'Does the client have a history of substance use disorder that prevents them from living independently without support services?', + 'c_rrh_transfer_federal_benefits' => 'Does the client qualify for federal benefits?', + 'c_rrh_transfer_eviction_history' => 'Does the client have one eviction within the last 5 years?', + 'c_rrh_transfer_eviction_history_disabilty_cause' => 'Were any previous evictions due to a disability or substance use disorder', + 'c_transfer_rrh_enrollment_moves' => 'How many times has the client moved while enrolled in rapid Re-housing?', + 'c_latest_date_financial_assistance_eligibility_rrh' => 'Enter date for last day of financial assistance', } end end diff --git a/app/views/clients/cas_readiness/edit.haml b/app/views/clients/cas_readiness/edit.haml index 719792f4f03..98f1ef82238 100644 --- a/app/views/clients/cas_readiness/edit.haml +++ b/app/views/clients/cas_readiness/edit.haml @@ -10,7 +10,9 @@ %p The following information is used exclusively for matching clients to vacancies in CAS. It is not used for federal reporting. - - = render 'background_render', url: render_content_client_cas_readiness_path, fetch_params: { client_id: @client.id } do - .rollup - .rollup-container.well + - if params[:render_inline] == "1" && Rails.env.development? + = render 'render_content' + - else + = render 'background_render', url: render_content_client_cas_readiness_path, fetch_params: { client_id: @client.id } do + .rollup + .rollup-container.well diff --git a/app/views/clients/rollup/_assessments.html.haml b/app/views/clients/rollup/_assessments.html.haml index f366f152664..9eaf9ed5cf8 100644 --- a/app/views/clients/rollup/_assessments.html.haml +++ b/app/views/clients/rollup/_assessments.html.haml @@ -5,7 +5,7 @@ - if RailsDrivers.loaded.include?(:eccovia_data) - eccovia_assessments = @client.source_eccovia_assessments - if @client.source_assessments.exists? - - ce_assessments = @client.source_assessments.preload(:assessment_questions).to_a + - ce_assessments = @client.source_assessments.order(assessment_date: :desc).preload(:assessment_questions).to_a - custom_hmis_assessments = [] - if HmisEnforcement.hmis_enabled? - custom_hmis_assessments = @client.hmis_source_custom_assessments.order(assessment_date: :desc).preload(:user, enrollment: [:data_source, { project: :warehouse_project }], form_processor: :definition).to_a From ccf9513c2a24d948e0c2466d9441c307ebe976c5 Mon Sep 17 00:00:00 2001 From: Dave G <149399758+dtgreiner@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:56:25 -0500 Subject: [PATCH 10/10] validate state level homelessness report filter spans at least 1 year & add public reporting tests (#5114) * validate state level homelessness report filter spans at least 1 year & add public reporting tests * minor updates --- .../public_reports_controller_concern.rb | 20 ++- .../state_level_homelessness.rb | 8 ++ .../spec/models/public_reports_spec.rb | 133 ++++++++++++++++++ 3 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 drivers/public_reports/spec/models/public_reports_spec.rb diff --git a/drivers/public_reports/app/controllers/public_reports/warehouse_reports/public_reports_controller_concern.rb b/drivers/public_reports/app/controllers/public_reports/warehouse_reports/public_reports_controller_concern.rb index b62bda5441f..0f50d4c49aa 100644 --- a/drivers/public_reports/app/controllers/public_reports/warehouse_reports/public_reports_controller_concern.rb +++ b/drivers/public_reports/app/controllers/public_reports/warehouse_reports/public_reports_controller_concern.rb @@ -28,13 +28,19 @@ def create user_id: current_user.id, state: :queued, } - @report = report_source.create(options) - ::WarehouseReports::GenericReportJob.perform_later( - user_id: current_user.id, - report_class: @report.class.name, - report_id: @report.id, - ) - respond_with(@report, location: path_to_report_index) + @report = report_source.new(options) + if @report.valid? + @report.save! + ::WarehouseReports::GenericReportJob.perform_later( + user_id: current_user.id, + report_class: @report.class.name, + report_id: @report.id, + ) + respond_with(@report, location: path_to_report_index) + else + flash[:error] = @report.errors.messages.values.join('; ') + redirect_to({ action: :index, filters: filter_params[:filters] }) + end end def update diff --git a/drivers/public_reports/app/models/public_reports/state_level_homelessness.rb b/drivers/public_reports/app/models/public_reports/state_level_homelessness.rb index 53b775910e3..edb66089fad 100644 --- a/drivers/public_reports/app/models/public_reports/state_level_homelessness.rb +++ b/drivers/public_reports/app/models/public_reports/state_level_homelessness.rb @@ -14,6 +14,14 @@ class StateLevelHomelessness < ::PublicReports::Report include Memery acts_as_paranoid + validate :validate_filter_dates_span_one_year, on: :create + + def validate_filter_dates_span_one_year + return if filter_object.start + 1.years <= filter_object.end + + errors.add(:base, 'The start and end dates must span at least one year.') + end + MIN_THRESHOLD = 11 attr_accessor :map_max_rate, :map_max_count diff --git a/drivers/public_reports/spec/models/public_reports_spec.rb b/drivers/public_reports/spec/models/public_reports_spec.rb new file mode 100644 index 00000000000..2fcfd55889b --- /dev/null +++ b/drivers/public_reports/spec/models/public_reports_spec.rb @@ -0,0 +1,133 @@ +### +# Copyright 2016 - 2025 Green River Data Analysis, LLC +# +# License detail: https://github.com/greenriver/hmis-warehouse/blob/production/LICENSE.md +### + +require 'rails_helper' + +RSpec.describe PublicReports::Report, type: :request do + before(:all) do + HmisCsvImporter::Utility.clear! + GrdaWarehouse::Utility.clear! + end + + let(:user) { create :user } + let(:option_lengths) do + { + options_3y: { + start: Date.parse('2021-01-01'), + end: Date.parse('2024-01-01'), + }, + options_1y: { + start: Date.parse('2021-01-01'), + end: Date.parse('2022-01-01'), + }, + options_1m_jan: { + start: Date.parse('2021-01-01'), + end: Date.parse('2021-01-31'), + }, + options_1m_may: { + start: Date.parse('2021-05-01'), + end: Date.parse('2021-05-31'), + }, + } + end + + describe 'smoke tests' do + before(:each) do + sign_in user + end + let(:public_reports) do + { + homeless_count_comparison: PublicReports::HomelessCountComparison, + homeless_count: PublicReports::HomelessCount, + homeless_population: PublicReports::HomelessPopulation, + number_housed: PublicReports::NumberHoused, + pit_by_month: PublicReports::PitByMonth, + point_in_time: PublicReports::PointInTime, + state_level_homelessness: PublicReports::StateLevelHomelessness, + } + end + + it 'public reports can build and run with 3 year filter' do + public_reports.values.each do |report_source| + run_report( + report_source: report_source, + options: get_report_options(options_type: :options_3y), + ) + expect(report_source.all.count).to eq(1) + end + end + + it 'public reports can build and run with 1 year filter' do + public_reports.values.each do |report_source| + run_report( + report_source: report_source, + options: get_report_options(options_type: :options_1y), + ) + expect(report_source.all.count).to eq(1) + end + end + + it 'public reports can build and run with 1 month (including January PIT date) filter' do + public_reports.except(:state_level_homelessness).values.each do |report_source| + run_report( + report_source: report_source, + options: get_report_options(options_type: :options_1m_jan), + ) + expect(report_source.all.count).to eq(1) + end + end + + it 'public reports fail to build and run with 1 month span (including January PIT date) filter' do + expect do + run_report( + report_source: public_reports[:state_level_homelessness], + options: get_report_options(options_type: :options_1m_jan), + ) + end.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'public reports can build and run with 1 month (not including a PIT date) filter' do + public_reports.except(:state_level_homelessness).values.each do |report_source| + run_report( + report_source: report_source, + options: get_report_options(options_type: :options_1m_may), + ) + expect(report_source.all.count).to eq(1) + end + end + + it 'public reports fail to build and run with 1 month span (not including a PIT date) filter' do + expect do + run_report( + report_source: public_reports[:state_level_homelessness], + options: get_report_options(options_type: :options_1m_may), + ) + end.to raise_error(ActiveRecord::RecordInvalid) + end + + def get_report_options(options_type:) + params = { enforce_one_year_range: false }.merge(option_lengths[options_type]) + filter = ::Filters::FilterBase.new(user_id: user.id).update(params) + { + start_date: filter.start, + end_date: filter.end, + filter: filter.for_params, + user_id: user.id, + } + end + + def run_report(report_source:, options:) + report = report_source.new(options) + + report.save! + ::WarehouseReports::GenericReportJob.perform_now( + user_id: user.id, + report_class: report.class.name, + report_id: report.id, + ) + end + end +end