From 700af021f1d50d2457573e3b813d9486e5c1ba03 Mon Sep 17 00:00:00 2001 From: Joseph Kotanchik Date: Tue, 14 May 2024 10:54:27 -0400 Subject: [PATCH 1/3] MAT-6835: Top level summary file --- Gemfile | 12 +- service/{app.rb => web_controller.rb} | 177 ++++++++++------ .../{app_test.rb => web_controller_test.rb} | 11 +- views/index.erb | 96 --------- views/top_level_summary.erb | 200 ++++++++++++++++++ 5 files changed, 328 insertions(+), 168 deletions(-) rename service/{app.rb => web_controller.rb} (60%) rename test/service/{app_test.rb => web_controller_test.rb} (80%) delete mode 100644 views/index.erb create mode 100644 views/top_level_summary.erb diff --git a/Gemfile b/Gemfile index 7b21920..9254560 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,14 @@ source "https://rubygems.org" -gem "sinatra" +gem 'sinatra' gem 'passenger' -gem "rest-client" -gem "cqm-reports", '4.1.0' -gem "rackup", "~> 2.1" -gem "rack-contrib", "~> 2.4" +gem 'rest-client' +gem 'cqm-reports', '4.1.0' +gem 'rackup', '~> 2.1' +gem 'rack-contrib', '~> 2.4' gem 'jwt' -gem "cqm-models", :git => "https://github.com/projecttacoma/cqm-models", :branch => "master" +gem 'cqm-models', :git => 'https://github.com/projecttacoma/cqm-models', :branch => 'master' group :test do gem 'minitest' diff --git a/service/app.rb b/service/web_controller.rb similarity index 60% rename from service/app.rb rename to service/web_controller.rb index 387ea70..f974661 100644 --- a/service/app.rb +++ b/service/web_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "bundler/setup" -require "sinatra" -require "cqm-reports" -require "cqm/models" +require 'bundler/setup' +require 'sinatra' +require 'cqm-reports' +require 'cqm/models' require 'rest-client' -require "rack" -require "rack/contrib" +require 'rack' +require 'rack/contrib' # Includes the JSONBodyParser middleware require 'jwt' puts "Loading QRDA Export Service" @@ -26,11 +26,18 @@ def as_json(*args) use Rack::JSONBodyParser -SCORING = { - "Proportion" => "PROPORTION", - "Ratio" => "RATIO", - "Cohort" => "COHORT", - "Continuous Variable" => "CONTINUOUS_VARIABLE" +POPULATION_ABBR = { + "initialPopulation" => "IPP", + "measurePopulation" => "MSRPOPL", + "measurePopulationExclusion" => "MSRPOPLEX", + "denominator" => "DENOM", + "numerator" => "NUMER", + "numeratorExclusion" => "NUMEX", + "denominatorException" => "DENEXCEP", + "denominatorExclusion" => "DENEX", + "stratification" => "STRAT", + "measureObservation" => "OBSERV", + "measurePopulationObservation" => "OBSERV" } # Implementation pre-reqs @@ -54,59 +61,37 @@ def as_json(*args) # 9. Log formatting put "/api/qrda" do + # Set return type content_type 'application/json' - #TODO probably don't need access token here, will remove after SME confirmation + # TODO probably don't need access token here, will remove after SME confirmation access_token = request.env["HTTP_Authorization"] + + # Parse request params measure_dto = request.params + # Prepare CQM Measure madie_measure = JSON.parse(measure_dto["measure"]) - measure = CQM::Measure.new(madie_measure) unless madie_measure.nil? + measure = CQM::Measure.new(madie_measure) unless measure_dto["measure"].nil? if measure.nil? return [400, "Measure is empty."] end - - test_cases = measure_dto["testCases"] - source_data_criteria = measure_dto["sourceDataCriteria"] - - data_criteria = Array.new - source_data_criteria.each do | criteria | - data_criteria.push build_source_data_criteria(criteria) - end - - measure.source_data_criteria = data_criteria + measure.source_data_criteria = build_source_data_criteria(measure_dto["sourceDataCriteria"]) measure.cms_id = measure.cms_id.nil? ? 'CMS0v0' : measure.cms_id measure.hqmf_id = madie_measure["id"] + test_cases = measure_dto["testCases"] + qrda_errors = {} html_errors = {} patients = Array.new - individual_reports = Array.new + generated_reports = Array.new # Array of each Patient's QRDA and HTML summary + # Generate QRDA XMLs and HTML patient summaries test_cases.each_with_index do | test_case, idx | - qdm_patient = QDM::Patient.new(JSON.parse(test_case["json"])) - - patient = CQM::Patient.new - patient.qdmPatient = qdm_patient - patient[:givenNames] = [ test_case["title"] ] - patient[:familyName] = test_case["series"] + patient = build_cqm_patient(idx, test_case) patients.push patient # For the summary HTML - expected_values = Array.new - if test_case["groupPopulations"] - test_case["groupPopulations"].each do | groupPopulation | - groupPopulation["populationValues"].each do | populationValue | - expected_values.push(populationValue["expected"]) - end - end - end - patient[:expectedValues] = expected_values - - if patient.qdmPatient.get_data_elements('patient_characteristic', 'payer').empty? - payer_codes = [{ 'code' => '1', 'system' => '2.16.840.1.113883.3.221.5', 'codeSystem' => 'SOP' }] - patient.qdmPatient.dataElements.push QDM::PatientCharacteristicPayer.new(dataElementCodes: payer_codes, relevantPeriod: QDM::Interval.new(patient.qdmPatient.birthDatetime, nil)) - end - filename = "#{idx+1}_#{patient[:familyName]}_#{patient[:givenNames][0]}" # generate QRDA @@ -122,24 +107,67 @@ def as_json(*args) rescue Exception => e html_errors[patient.id] = e end - individual_reports.push << {filename:, qrda:, report:} + generated_reports.push << {filename:, qrda:, report:} end - summary_report = measure_patients_summary(patients, nil, qrda_errors, html_errors, measure) - { summaryReport: summary_report,individualReports: individual_reports }.to_json + summary_report = summary_report(patients, + qrda_errors, + html_errors, + measure, + measure_dto["testCaseDtos"]) + + return { summaryReport: summary_report, individualReports: generated_reports }.to_json +end + +def summary_report(patients, qrda_errors, html_errors, measure, population_results) + erb "top_level_summary".to_sym, {}, { + measure: , + records: patients, + html_errors: , + qrda_errors: , + population_crit_results: population_results, + population_abbr: POPULATION_ABBR + } end + get "/api/health" do puts "QRDA Export Service is up" "QRDA Export Service is up" end def build_source_data_criteria(source_data_criteria) - data_criteria = instantiate_model(source_data_criteria["type"]) - data_criteria.codeListId = source_data_criteria["oid"] - data_criteria.description = source_data_criteria["description"] + data_criteria = Array.new + source_data_criteria.each do | criteria | + data_criteria.push map_source_data_criteria(criteria) + end data_criteria end +def map_source_data_criteria(criteria) + data_criteria = instantiate_model(criteria["type"]) + data_criteria.codeListId = criteria["oid"] + data_criteria.description = criteria["description"] + data_criteria +end + +def build_cqm_patient(idx, test_case) + qdm_patient = QDM::Patient.new(JSON.parse(test_case["json"])) + + patient = CQM::Patient.new + patient[:id] = idx + patient.qdmPatient = qdm_patient + patient[:givenNames] = [test_case["title"]] + patient[:familyName] = test_case["series"] + patient[:pass] = true + + if patient.qdmPatient.get_data_elements('patient_characteristic', 'payer').empty? + payer_codes = [{ 'code' => '1', 'system' => '2.16.840.1.113883.3.221.5', 'codeSystem' => 'SOP' }] + patient.qdmPatient.dataElements.push QDM::PatientCharacteristicPayer.new(dataElementCodes: payer_codes, + relevantPeriod: QDM::Interval.new(patient.qdmPatient.birthDatetime, nil)) + end + patient +end + def instantiate_model(model_name) case model_name when "PatientEntity" @@ -263,12 +291,41 @@ def instantiate_model(model_name) end end -def measure_patients_summary(patients, results, qrda_errors, html_errors, measure) - erb "index".to_sym, {}, { - measure: measure, - results: results, - records: patients, - html_errors: html_errors, - qrda_errors: qrda_errors - } -end \ No newline at end of file +# def prepare_patient_summary(idx, patient, test_case) +# patient_summary = { +# id: idx, +# family_name: patient[:familyName], +# given_name: patient[:givenNames][0], +# pass: true, +# group_population_results: Array.new +# } +# +# if test_case["groupPopulations"] +# test_case["groupPopulations"].each do |groupPopulation| +# groupPopulation["populationValues"].each do |populationValue| +# # Convert expected/actual values to integers (matches legacy output) +# expected = populationValue["expected"] if populationValue["expected"].is_a? Integer +# actual = populationValue["actual"] if populationValue["actual"].is_a? Integer +# +# if expected.nil? and [true, false].include? populationValue["expected"] +# expected = populationValue["expected"] ? 1 : 0 +# end +# +# if actual.nil? and [true, false].include? populationValue["actual"] +# actual = populationValue["actual"] ? 1 : 0 +# end +# +# population_results = { +# name: POPULATION_ABBR[populationValue["name"]], +# expected: expected, +# actual: actual, +# result: expected == actual ? 'pass' : 'fail', # maps to desired css styling class +# unit: "" +# } +# patient_summary[:pass] = population_results[:result] == 'pass' if patient_summary[:pass] +# patient_summary[:group_population_results].push population_results +# end +# end +# end +# patient_summary +# end \ No newline at end of file diff --git a/test/service/app_test.rb b/test/service/web_controller_test.rb similarity index 80% rename from test/service/app_test.rb rename to test/service/web_controller_test.rb index 8fedc92..682dddd 100644 --- a/test/service/app_test.rb +++ b/test/service/web_controller_test.rb @@ -1,11 +1,11 @@ ENV['APP_ENV'] = 'test' # $LOAD_PATH.unshift File.expand_path('../../service', __FILE__) -require_relative '../../service/app' +require_relative '../../service/web_controller' require 'minitest/autorun' require 'rack/test' -class AppTest < Minitest::Test +class WebControllerTest < Minitest::Test include Rack::Test::Methods def app @@ -20,10 +20,10 @@ def test_has_a_health_check def test_qrda_with_empty_payload put 'api/qrda' - + assert_equal 400, last_response.status end - def test_build_source_data_criteria + def test_map_source_data_criteria criteria = { "oid" => "2.16.840.1.113762.1.4.1151.59", "title" => "Hospital Services for Urology", @@ -33,10 +33,9 @@ def test_build_source_data_criteria "codeId" => nil, "name" => "Hospital Services for Urology" } - source_criteria = build_source_data_criteria(criteria) + source_criteria = map_source_data_criteria(criteria) assert source_criteria.is_a?(QDM::EncounterPerformed) assert_equal criteria["oid"], source_criteria.codeListId assert_equal criteria["description"], source_criteria.description end - end \ No newline at end of file diff --git a/views/index.erb b/views/index.erb deleted file mode 100644 index e881432..0000000 --- a/views/index.erb +++ /dev/null @@ -1,96 +0,0 @@ - - - - - -

: TEST CASE RESULTS BY MEASURE

-

<%=measure.cms_id%>: <%=measure.title%>

- - - - \ No newline at end of file diff --git a/views/top_level_summary.erb b/views/top_level_summary.erb new file mode 100644 index 0000000..18eded3 --- /dev/null +++ b/views/top_level_summary.erb @@ -0,0 +1,200 @@ + + + + + +

: TEST CASE RESULTS BY MEASURE

+

<%=measure.cms_id%>: <%=measure.title%>

+ +<% population_crit_results.each_with_index do |pop_crit, pop_crit_idx| %> + +
+ <% if population_crit_results.length > 1%> +

Population: <%= pop_crit["groupNumber"] %>

+ <% end %> + + + + + + + + <% + result = pop_crit["testCaseExecutionResults"].find { |result| result["populations"].find {| pop | pop["pass"] == false} } ? 'fail' : 'pass' + %> + + <%# passed_count = 0 %> + <%# pop_crit["testCaseExecutionResults"].each do |test_case| %> + <%# passed_count += 1 unless test_case["populations"].find { |pop| pop["pass"] == false} %> + <%# end %> + <%# total = pop_crit[:pass_fail_ratio].split('/')[1] %> + + + + + + + + + + + +
RESULTCATEGORY
<%= result %><%= nil %>/<%= pop_crit["testCaseExecutionResults"].length %>
<%= nil %>%Passing
<%= nil %>%Coverage
+
+
+ <% pop_crit["testCaseExecutionResults"].each_with_index do |patient_summary, index| %> + <%# patient_result = result[1]['differences'].values.select{|difference| difference['medicalRecordNumber'] == record.qdmPatient.id.to_s}.first %> +
+ + + + +
+

+ <% result = patient_summary["populations"].find {|pop| pop["pass"] == false}.nil? ? 'pass' : 'fail' %> + <%= result %>: + <% unless html_errors[patient_summary["testCaseId"]] %> + "><%=patient_summary["last"]%>, <%=patient_summary["first"]%> + <% else %> + <%=patient_summary[:family_name]%>, <%=patient_summary[:given_name]%> + <% end %> + <% unless qrda_errors[patient_summary[:id]] %> + ">qrda + <% end %> +

+
+ <% if qrda_errors[patient_summary[:id]] %> +
+ There was an error exporting the QRDA for this Test Case: + <% data_criteria = qrda_errors[patient_summary[:id]].data_criteria rescue nil %> + <% if data_criteria %> + could not generate appropriate QRDA content for <%= data_criteria.description %> + (<%= qrda_errors[patient_summary[:id]].message %>) + <% else %> + <%= qrda_errors[patient_summary[:id]].message %> + <% end %> +
+ <% end %> + <% if html_errors[patient_summary[:id]] %> +
+ There was an error exporting the HTML for this Test Case: + <% data_criteria = html_errors[patient_summary[:id]].data_criteria rescue nil %> + <% if data_criteria %> + could not generate appropriate HTML content for <%= data_criteria.description %> + (<%= html_errors[patient_summary[:id]].message %>) + <% else %> + <%= html_errors[patient_summary[:id]].message %> + <% end %> +
+ <% end %> + + + + + + + + <% patient_summary["populations"].each do |pop| %> + <% pop["name"] = 'OBSERV_1' if pop["name"] == 'OBSERV'%> + + <% if pop["pass"] %> + + <% else %> + + <% end %> + + + + + <% end %> +
StatusPopulationExpectedActual
×<%= population_abbr[pop["name"]] %><%= pop["expected"] %><%= pop["unit"] %><%= pop["actual"] %><%= pop["unit"] %>
+ <% end %> +

+<% end %> + + \ No newline at end of file From 90931b7d1c196d559b42bb4f95106866c51a4bfc Mon Sep 17 00:00:00 2001 From: Joseph Kotanchik Date: Thu, 30 May 2024 13:40:42 -0400 Subject: [PATCH 2/3] MAT-6835: Use QRDA DTOs and clean up. --- service/web_controller.rb | 43 ++-------------------------------- views/top_level_summary.erb | 46 ++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 64 deletions(-) diff --git a/service/web_controller.rb b/service/web_controller.rb index f974661..4803429 100644 --- a/service/web_controller.rb +++ b/service/web_controller.rb @@ -68,7 +68,7 @@ def as_json(*args) access_token = request.env["HTTP_Authorization"] # Parse request params - measure_dto = request.params + measure_dto = request.params # Uses the Rack::JSONBodyParser middleware # Prepare CQM Measure madie_measure = JSON.parse(measure_dto["measure"]) @@ -113,7 +113,7 @@ def as_json(*args) qrda_errors, html_errors, measure, - measure_dto["testCaseDtos"]) + measure_dto["groupDTOs"]) return { summaryReport: summary_report, individualReports: generated_reports }.to_json end @@ -290,42 +290,3 @@ def instantiate_model(model_name) raise "Unsupported data type: #{model_name}" end end - -# def prepare_patient_summary(idx, patient, test_case) -# patient_summary = { -# id: idx, -# family_name: patient[:familyName], -# given_name: patient[:givenNames][0], -# pass: true, -# group_population_results: Array.new -# } -# -# if test_case["groupPopulations"] -# test_case["groupPopulations"].each do |groupPopulation| -# groupPopulation["populationValues"].each do |populationValue| -# # Convert expected/actual values to integers (matches legacy output) -# expected = populationValue["expected"] if populationValue["expected"].is_a? Integer -# actual = populationValue["actual"] if populationValue["actual"].is_a? Integer -# -# if expected.nil? and [true, false].include? populationValue["expected"] -# expected = populationValue["expected"] ? 1 : 0 -# end -# -# if actual.nil? and [true, false].include? populationValue["actual"] -# actual = populationValue["actual"] ? 1 : 0 -# end -# -# population_results = { -# name: POPULATION_ABBR[populationValue["name"]], -# expected: expected, -# actual: actual, -# result: expected == actual ? 'pass' : 'fail', # maps to desired css styling class -# unit: "" -# } -# patient_summary[:pass] = population_results[:result] == 'pass' if patient_summary[:pass] -# patient_summary[:group_population_results].push population_results -# end -# end -# end -# patient_summary -# end \ No newline at end of file diff --git a/views/top_level_summary.erb b/views/top_level_summary.erb index 18eded3..727aded 100644 --- a/views/top_level_summary.erb +++ b/views/top_level_summary.erb @@ -91,7 +91,7 @@

: TEST CASE RESULTS BY MEASURE

<%=measure.cms_id%>: <%=measure.title%>

-<% population_crit_results.each_with_index do |pop_crit, pop_crit_idx| %> +<% population_crit_results.each do |pop_crit| %>
<% if population_crit_results.length > 1%> @@ -105,29 +105,29 @@ <% - result = pop_crit["testCaseExecutionResults"].find { |result| result["populations"].find {| pop | pop["pass"] == false} } ? 'fail' : 'pass' + result = pop_crit["testCaseDTOs"].find { |result| result["populations"].find {| pop | pop["pass"] == false} } ? 'fail' : 'pass' %> <%= result %> - <%# passed_count = 0 %> - <%# pop_crit["testCaseExecutionResults"].each do |test_case| %> - <%# passed_count += 1 unless test_case["populations"].find { |pop| pop["pass"] == false} %> - <%# end %> - <%# total = pop_crit[:pass_fail_ratio].split('/')[1] %> - <%= nil %>/<%= pop_crit["testCaseExecutionResults"].length %> + <% passed_count = 0 %> + <% total = pop_crit["testCaseDTOs"].length %> + <% pop_crit["testCaseDTOs"].each do |test_case| %> + <% passed_count += 1 unless test_case["populations"].find { |pop| pop["pass"] == false} %> + <% end %> + <%= passed_count %>/<%= total %> - <%= nil %>% + <%= ((passed_count.to_f / total) * 100).floor %>% Passing - <%= nil %>% + <%= pop_crit["coverage"] %>% Coverage

- <% pop_crit["testCaseExecutionResults"].each_with_index do |patient_summary, index| %> + <% pop_crit["testCaseDTOs"].each_with_index do |patient_summary, index| %> <%# patient_result = result[1]['differences'].values.select{|difference| difference['medicalRecordNumber'] == record.qdmPatient.id.to_s}.first %>
@@ -137,38 +137,38 @@ <% result = patient_summary["populations"].find {|pop| pop["pass"] == false}.nil? ? 'pass' : 'fail' %> <%= result %>: <% unless html_errors[patient_summary["testCaseId"]] %> - "><%=patient_summary["last"]%>, <%=patient_summary["first"]%> + "><%=patient_summary["lastName"]%>, <%=patient_summary["firstName"]%> <% else %> - <%=patient_summary[:family_name]%>, <%=patient_summary[:given_name]%> + <%=patient_summary["lastName"]%>, <%=patient_summary["firstName"]%> <% end %> - <% unless qrda_errors[patient_summary[:id]] %> - ">qrda + <% unless qrda_errors[patient_summary["testCaseId"]] %> + ">qrda <% end %>
- <% if qrda_errors[patient_summary[:id]] %> + <% if qrda_errors[patient_summary["testCaseId"]] %>
There was an error exporting the QRDA for this Test Case: - <% data_criteria = qrda_errors[patient_summary[:id]].data_criteria rescue nil %> + <% data_criteria = qrda_errors[patient_summary["testCaseId"]].data_criteria rescue nil %> <% if data_criteria %> could not generate appropriate QRDA content for <%= data_criteria.description %> - (<%= qrda_errors[patient_summary[:id]].message %>) + (<%= qrda_errors[patient_summary["testCaseId"]].message %>) <% else %> - <%= qrda_errors[patient_summary[:id]].message %> + <%= qrda_errors[patient_summary["testCaseId"]].message %> <% end %>
<% end %> - <% if html_errors[patient_summary[:id]] %> + <% if html_errors[patient_summary["testCaseId"]] %>
There was an error exporting the HTML for this Test Case: - <% data_criteria = html_errors[patient_summary[:id]].data_criteria rescue nil %> + <% data_criteria = html_errors[patient_summary["testCaseId"]].data_criteria rescue nil %> <% if data_criteria %> could not generate appropriate HTML content for <%= data_criteria.description %> - (<%= html_errors[patient_summary[:id]].message %>) + (<%= html_errors[patient_summary["testCaseId"]].message %>) <% else %> - <%= html_errors[patient_summary[:id]].message %> + <%= html_errors[patient_summary["testCaseId"]].message %> <% end %>
<% end %> From d20792131eb11ca6363f902df06b590576e55df5 Mon Sep 17 00:00:00 2001 From: Joseph Kotanchik Date: Fri, 31 May 2024 10:12:09 -0400 Subject: [PATCH 3/3] Update rack config --- config.ru | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.ru b/config.ru index 48681dd..e38c576 100644 --- a/config.ru +++ b/config.ru @@ -2,5 +2,5 @@ require 'bundler' Bundler.require -require './service/app' +require './service/web_controller' run Sinatra::Application \ No newline at end of file