Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Metrics/ClassLength:
- 'lib/cypress/demographics_randomizer.rb'
- 'lib/cypress/expected_results_calculator.rb'
- 'lib/cypress/population_clone_job.rb'
- 'lib/cypress/scoop_and_filter.rb'
- 'lib/validators/cat3_population_validator.rb'
Metrics/CyclomaticComplexity:
Max: 12
Expand Down
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ group :development, :test do
# remove scss_lint, incompatible with sass dependency upgrades
# gem 'scss_lint', require: false
gem 'selenium-webdriver'
gem 'webrick'
end

group :development do
Expand Down
17 changes: 3 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ GEM
railties (>= 6.1.0)
faker (3.8.0)
i18n (>= 1.8.11, < 2)
ffi (1.17.4)
ffi (1.17.4-aarch64-linux-gnu)
ffi (1.17.4-aarch64-linux-musl)
ffi (1.17.4-arm-linux-gnu)
Expand Down Expand Up @@ -350,7 +349,7 @@ GEM
local_time (3.0.3)
log4r (1.1.10)
logger (1.7.0)
loofah (2.25.1)
loofah (2.25.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
macaddr (1.7.2)
Expand All @@ -373,7 +372,6 @@ GEM
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.27.0)
minitest-rails (8.1.0)
minitest (~> 5.20)
Expand Down Expand Up @@ -415,9 +413,6 @@ GEM
newrelic_rpm (10.4.0)
logger
nio4r (2.7.5)
nokogiri (1.19.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.19.4-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-aarch64-linux-musl)
Expand Down Expand Up @@ -499,8 +494,8 @@ GEM
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
rails-html-sanitizer (1.7.1)
loofah (~> 2.25, >= 2.25.2)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-perftest (0.0.7)
rails_best_practices (1.23.4)
Expand Down Expand Up @@ -578,9 +573,6 @@ GEM
logger
ruby2_keywords (0.0.5)
rubyzip (1.3.0)
sass-embedded (1.99.0)
google-protobuf (~> 4.31)
rake (>= 13)
sass-embedded (1.99.0-aarch64-linux-gnu)
google-protobuf (~> 4.31)
sass-embedded (1.99.0-aarch64-linux-musl)
Expand Down Expand Up @@ -669,7 +661,6 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2)
websocket (1.2.11)
websocket-driver (0.8.2)
base64
Expand All @@ -689,7 +680,6 @@ PLATFORMS
arm-linux-musl
arm-linux-musleabihf
arm64-darwin
ruby
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
Expand Down Expand Up @@ -771,7 +761,6 @@ DEPENDENCIES
vmstat
web-console (~> 4.2.0)
webmock
webrick

RUBY VERSION
ruby 3.4.9p82
Expand Down
6 changes: 4 additions & 2 deletions app/models/product_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,13 @@ def generate_patients(job_id = nil)
if product.randomize_patients
# If we're using a "slim test deck", don't pass in any random IDs
random_ids = slim_test_deck? ? [] : gather_patient_ids
Cypress::PopulationCloneJob.new('test_id' => id, 'patient_ids' => master_patient_ids, 'randomization_ids' => random_ids,
Cypress::PopulationCloneJob.new('test_id' => id, 'patient_ids' => master_patient_ids,
'scoop_and_filter' => true, 'randomization_ids' => random_ids,
'include_virtual' => (measure_ids & APP_CONSTANTS['telehealth_ineligible_measures']),
'randomize_demographics' => true, 'generate_provider' => product.c4_test, 'job_id' => job_id).perform
else
Cypress::PopulationCloneJob.new('test_id' => id, 'patient_ids' => master_patient_ids, 'disable_randomization' => true).perform
Cypress::PopulationCloneJob.new('test_id' => id, 'patient_ids' => master_patient_ids,
'scoop_and_filter' => true, 'disable_randomization' => true).perform
end
end

Expand Down
2 changes: 1 addition & 1 deletion features/support/env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def default_drivers

default_drivers

Capybara.server = :webrick
Capybara.server = :puma, { Silent: true }
Capybara.default_max_wait_time = 15
# Capybara.ignore_hidden_elements = false

Expand Down
2 changes: 1 addition & 1 deletion lib/cypress/patient_zipper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def self.zip(file, patients, format)
sf_patient.id = patient.id
# CMS529 requires that entries point to the ecounters that they are related to
sf_patient.add_encounter_ids_to_events if measures.map(&:hqmf_id).intersect?(APP_CONSTANTS['result_measures'].map(&:hqmf_id))
patient_scoop_and_filter.scoop_and_filter(sf_patient)
patient_scoop_and_filter.replace_negated_codes_with_valueset(sf_patient)
z.put_next_entry("#{next_entry_path(patient, i)}.#{FORMAT_EXTENSIONS[format.to_sym]}")
z << formatter.export(sf_patient)
end
Expand Down
3 changes: 3 additions & 0 deletions lib/cypress/population_clone_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def perform
end
end

@patient_scoop_and_filter = Cypress::ScoopAndFilter.new(@test.measures) if @options['scoop_and_filter']

# get single provider if @test is a measure test. measure tests have a single provider for each patient while filtering tests can have different
# providers for each patient
provider = @test.provider if @test.instance_of?(MeasureTest)
Expand Down Expand Up @@ -96,6 +98,7 @@ def clone_and_save_patient(patient, prng, provider = nil, allow_dups: false)
original_patient_id: patient.id,
correlation_id: options['test_id']
}
@patient_scoop_and_filter&.scoop_and_filter(cloned_patient, replace_negations: false, persist_scoop: true)
unnumerify cloned_patient if patient.givenNames.map { |n| n =~ /\d/ }.any? || patient.familyName =~ /\d/
cloned_patient.medical_record_number = next_medical_record_number unless options['disable_randomization']
@test.reload
Expand Down
85 changes: 66 additions & 19 deletions lib/cypress/scoop_and_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,47 +20,92 @@ def get_non_demographic_category_statuses(measures)
end.flatten.uniq.compact
end

def scoop_and_filter(patient)
demographic_criteria = patient.qdmPatient.dataElements.collect { |de| de if de.qdmCategory == 'patient_characteristic' }.compact
def scoop_and_filter(patient, replace_negations: true, persist_scoop: false)
de_to_delete = []
de_to_delete += patient.qdmPatient.dataElements.map { |de| de unless data_element_used_by_measure(de) }
patient.qdmPatient.dataElements.each do |data_element|
scoop_and_filter_data_element_codes(data_element, persist_scoop)
end
# keep data element if codes is not empty
de_to_delete += patient.qdmPatient.dataElements.map { |de| de unless de.dataElementCodes.present? }
if persist_scoop
de_to_delete.compact.each(&:destroy)
else
ids_to_delete = de_to_delete.compact.map(&:id)
patient.qdmPatient.dataElements.keep_if { |data_element| !ids_to_delete.include?(data_element.id) }
end
replace_negated_codes_with_valueset(patient) if replace_negations
patient
end

def replace_negated_codes_with_valueset(patient)
# If a negated code belongs to multiple valuesets, we need to add a cloned entry for each valueset.
# This array stores the cloned entries to be added
multi_vs_negation_elements = []
patient.qdmPatient.dataElements.keep_if { |de| data_element_used_by_measure(de) }
ids_to_delete = []
patient.qdmPatient.dataElements.each do |data_element|
scoop_and_filter_data_element_codes(data_element, multi_vs_negation_elements, patient)
data_element.dataElementCodes.first
next unless data_element.respond_to?('negationRationale') && data_element.negationRationale

replace_negated_code_with_valueset(data_element, multi_vs_negation_elements)
if data_element.dataElementCodes.blank?
ids_to_delete << data_element.id
next
end

# add data element valueset and other potentially relevant valueset descriptions
codes = (multi_vs_negation_elements + [data_element]).map { |de| "#{de.dataElementCodes.first.code}:#{de.dataElementCodes.first.system}" }
Cypress::QrdaPostProcessor.build_code_descriptions(codes, patient, patient.bundle)
end
# keep data element if codes is not empty
patient.qdmPatient.dataElements.keep_if { |data_element| data_element.dataElementCodes.present? }
patient.qdmPatient.dataElements.concat(demographic_criteria)
patient.qdmPatient.dataElements.concat(multi_vs_negation_elements)
patient.qdmPatient.dataElements.keep_if { |data_element| !ids_to_delete.include?(data_element.id) }
patient
end

private

# Method to remove codes from a data element that are not relevant to measure.
# Multi_vs_negation_elements is an array of cloned elements to add to patient record to capture all of the negated valuesets
def scoop_and_filter_data_element_codes(data_element, multi_vs_negation_elements, patient)
def scoop_and_filter_data_element_codes(data_element, persist_scoop)
return if data_element.qdmCategory == 'patient_characteristic'

# keep if data_element code and codesystem is in one of the relevant_codes
# Also keep all negated valuesets, we'll deal with those later
data_element.dataElementCodes.keep_if do |de_code|
@relevant_codes.include?(code: de_code.code, system: de_code.system) || de_code.system == '1.2.3.4.5.6.7.8.9.10'
relevant_code?(de_code) || de_code.system == '1.2.3.4.5.6.7.8.9.10'
end
# Return if all codes have been removed
return if data_element.dataElementCodes.blank?

remove_irrelevant_valuesets_and_add_description_to_data_element(data_element)
# Return if all codes and valuesets have been removed
return if data_element.dataElementCodes.blank?
return unless data_element.respond_to?('negationRationale') && data_element.negationRationale
scoop_and_filter_data_element_fields(data_element)
# For repeatability, don't do this if you are saving the record
remove_irrelevant_valuesets_and_add_description_to_data_element(data_element) unless persist_scoop
end

replace_negated_code_with_valueset(data_element, multi_vs_negation_elements)
return if data_element.dataElementCodes.blank?
def scoop_and_filter_data_element_fields(data_element)
# Iterate through each field to see coded fields include relevant codes
data_element.fields.keys.each do |field_name|
next if data_element[field_name].nil?

# Diagnoses and Facility Locations are unique because they are arrays
if field_name == 'diagnoses'
data_element.diagnoses.keep_if do |diagnosis|
relevant_code?(diagnosis.code)
end
data_element.diagnoses = nil if data_element.diagnoses.blank?
elsif field_name == 'facilityLocations'
data_element.facilityLocations.keep_if do |facility_location|
relevant_code?(facility_location.code)
end
data_element.facilityLocations = nil if data_element.facilityLocations.blank?
end
next unless data_element.fields[field_name].type == QDM::Code

data_element[field_name] = nil unless relevant_code?(data_element[field_name])
end
end

# add data element valueset and other potentially relevant valueset descriptions
codes = (multi_vs_negation_elements + [data_element]).map { |de| "#{de.dataElementCodes.first.code}:#{de.dataElementCodes.first.system}" }
Cypress::QrdaPostProcessor.build_code_descriptions(codes, patient, patient.bundle)
def relevant_code?(code)
@relevant_codes.include?(code: code.code, system: code.system)
end

def data_element_category_and_status(data_element)
Expand All @@ -69,6 +114,8 @@ def data_element_category_and_status(data_element)

# returns true if a patients data element is used by a measure
def data_element_used_by_measure(data_element)
return true if data_element.qdmCategory == 'patient_characteristic'

!@de_category_statuses_for_measures.index do |dcs|
dcs[:category] == data_element['qdmCategory'] && dcs[:status] == data_element['qdmStatus']
end.nil?
Expand Down
2 changes: 1 addition & 1 deletion test/models/cat1_filter_task_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def test_task_good_results_should_pass
perform_enqueued_jobs do
te = task.execute(testfile, user)
te.reload
assert_empty te.execution_errors, 'test execution with known good results should have no errors'
assert_empty te.execution_errors.only_errors, 'test execution with known good results should have no errors'
end
end
end
3 changes: 1 addition & 2 deletions test/unit/lib/patient_zipper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ class PatientZipperTest < ActiveSupport::TestCase
doc = Nokogiri::XML(zip_entry.get_input_stream, &:strict)
doc.root.add_namespace_definition('cda', 'urn:hl7-org:v3')
doc.root.add_namespace_definition('sdtc', 'urn:hl7-org:sdtc')
# There should be 1 entry with encounter ids. The fixture measures do not have PhysicalExamPerformed criteria, so that entry will be filtered out
assert_equal 1, doc.xpath('//sdtc:templateId[@root="2.16.840.1.113883.10.20.24.3.150"]').size, 'There should be 1 entry with encounter ids'
assert_equal 2, doc.xpath('//sdtc:templateId[@root="2.16.840.1.113883.10.20.24.3.150"]').size, 'There should be 2 entry with encounter ids'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert_equal 2, doc.xpath('//sdtc:templateId[@root="2.16.840.1.113883.10.20.24.3.150"]').size, 'There should be 2 entry with encounter ids'
assert_equal 2, doc.xpath('//sdtc:templateId[@root="2.16.840.1.113883.10.20.24.3.150"]').size, 'There should be 2 entries with encounter ids'

count += 1
end
end
Expand Down
Loading