Skip to content

Commit

Permalink
Merge branch 'release-151' into ea/fy-2024-transfer-assessment
Browse files Browse the repository at this point in the history
  • Loading branch information
eanders authored Feb 5, 2025
2 parents 487a354 + 84f151c commit 6feef88
Show file tree
Hide file tree
Showing 19 changed files with 272 additions and 121 deletions.
3 changes: 2 additions & 1 deletion app/controllers/cohorts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def create

# If the user doesn't have All Cohorts access, grant them access to the cohort
@cohort.replace_access(current_user, scope: :editor)
@cohort.replace_access(current_user, scope: :viewer)
# Always add the cohort to the system group
AccessGroup.maintain_system_groups(group: :cohorts)
# Add default tabs
Expand All @@ -150,7 +151,7 @@ def create
end
end
# Search the list so you can see the newly created cohort
redirect_to cohorts_path('q[name_cont]' => @cohort.name)
redirect_to cohorts_path('search_form[q]' => @cohort.name)
rescue Exception => e
flash[:error] = e.message
redirect_to cohorts_path
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/notification_configurations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@ def destroy
helper_method :import_threshold

def notification_configuration
@notification_configuration ||= GrdaWarehouse::NotificationConfiguration.find_safely(params[:id]) ||
@notification_configuration ||= if params[:id].present?
GrdaWarehouse::NotificationConfiguration.find_safely(params[:id])
else
GrdaWarehouse::NotificationConfiguration.new(
source: import_threshold,
notification_slug: import_threshold.valid_notification_slug(params[:notification_slug]),
)
end
end
helper_method :notification_configuration
end
6 changes: 5 additions & 1 deletion app/controllers/project_groups_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def index

def new
@project_group = project_group_source.new
set_access
set_group_access
end

def create
Expand Down Expand Up @@ -166,6 +166,10 @@ def set_project_group

def set_access
@editor_ids = @project_group.editable_access_control.user_ids
set_group_access
end

def set_group_access
# TODO: START_ACL remove when ACL transition complete
@groups = @project_group.access_groups
@group_ids = @project_group.access_group_ids
Expand Down
2 changes: 1 addition & 1 deletion app/views/import_thresholds/show.haml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
= simple_form_for import_threshold, url: data_source_import_threshold_path, method: :patch do |f|
.well
%h2 Changes in Record Counts
%p The following settings apply when an import significantly changes the number of rows in the data that already exists in the warehouse for the data source. This will look at the aggregate change, so if 100 rows are removed and 100 added, no change is indicated.
%p The following settings apply when an import significantly changes the number of rows in the data that already exist in the warehouse for the data source that meet the criteria of the import. This will not trigger if no data exists in the warehouse for the included projects and date range. This will look at the aggregate change, so if 100 rows are removed and 100 added, no change is indicated.
.row
.col
= f.input :record_count_change_percent_threshold, label: 'Record count change threshold (%)', hint: 'Imports will pause and alert if more than the chosen percentage have changed', input_html: { style: 'width: 5em' }
Expand Down
9 changes: 8 additions & 1 deletion drivers/hmis/app/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion drivers/hmis/app/graphql/types/forms/enums/pick_list_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
26 changes: 22 additions & 4 deletions drivers/hmis/app/graphql/types/forms/pick_list_option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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|
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
111 changes: 111 additions & 0 deletions drivers/hmis/app/models/hmis/form/occurrence_point_form_collection.rb
Original file line number Diff line number Diff line change
@@ -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
46 changes: 4 additions & 42 deletions drivers/hmis/app/models/hmis/hud/enrollment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -339,49 +340,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!
Expand Down
12 changes: 1 addition & 11 deletions drivers/hmis/app/models/hmis/hud/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion drivers/hmis/spec/factories/hmis/form/definitions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 6feef88

Please sign in to comment.