health-data-standards 3.0.6 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +3 -1
- data/Rakefile +2 -0
- data/lib/health-data-standards.rb +10 -0
- data/lib/health-data-standards/export/cat_3.rb +24 -0
- data/lib/health-data-standards/export/html.rb +2 -1
- data/lib/health-data-standards/import/bundle/importer.rb +178 -146
- data/lib/health-data-standards/import/cat1/diagnosis_active_importer.rb +0 -5
- data/lib/health-data-standards/import/cat1/diagnosis_inactive_importer.rb +1 -6
- data/lib/health-data-standards/import/cat1/diagnostic_study_order_importer.rb +0 -4
- data/lib/health-data-standards/import/cat1/ecog_status_importer.rb +12 -0
- data/lib/health-data-standards/import/cat1/encounter_order_importer.rb +0 -5
- data/lib/health-data-standards/import/cat1/lab_order_importer.rb +1 -5
- data/lib/health-data-standards/import/cat1/lab_result_importer.rb +18 -0
- data/lib/health-data-standards/import/cat1/medication_active_importer.rb +27 -0
- data/lib/health-data-standards/import/cat1/patient_importer.rb +54 -46
- data/lib/health-data-standards/import/cat1/procedure_performed_importer.rb +28 -0
- data/lib/health-data-standards/import/cat1/symptom_active_importer.rb +12 -0
- data/lib/health-data-standards/import/cda/condition_importer.rb +2 -0
- data/lib/health-data-standards/import/cda/encounter_importer.rb +6 -0
- data/lib/health-data-standards/import/cda/procedure_importer.rb +1 -0
- data/lib/health-data-standards/import/cda/section_importer.rb +27 -5
- data/lib/health-data-standards/models/cda_identifier.rb +17 -0
- data/lib/health-data-standards/models/cqm/aggregate_objects.rb +92 -0
- data/lib/health-data-standards/models/cqm/measure.rb +51 -30
- data/lib/health-data-standards/models/cqm/query_cache.rb +64 -0
- data/lib/health-data-standards/models/entry.rb +1 -1
- data/lib/health-data-standards/models/record.rb +28 -3
- data/lib/health-data-standards/models/svs/value_set.rb +20 -0
- data/lib/health-data-standards/tasks/bundle.rake +18 -8
- data/lib/health-data-standards/util/hqmf_template_helper.rb +6 -2
- data/lib/health-data-standards/util/hqmf_template_oid_map.json +4 -0
- data/lib/health-data-standards/util/vs_api.rb +9 -6
- data/lib/hqmf-model/data_criteria.json +10 -10
- data/lib/hqmf-parser.rb +0 -2
- data/lib/hqmf-parser/1.0/range.rb +21 -9
- data/templates/c32/show.c32.erb +1 -1
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.4.cat1.erb +1 -0
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.40.cat1.erb +2 -1
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.44.cat1.erb +1 -0
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.46.cat1.erb +1 -1
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.54.cat1.erb +16 -1
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.62.cat1.erb +1 -1
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.69.cat1.erb +7 -2
- data/templates/cat1/_2.16.840.1.113883.10.20.24.3.76.cat1.erb +2 -1
- data/templates/cat1/_medication_details.cat1.erb +4 -4
- data/templates/cat1/_record_target.cat1.erb +2 -2
- data/templates/cat1/_result_value.cat1.erb +1 -1
- data/templates/cat1/show.cat1.erb +2 -2
- data/templates/cat3/_continuous_variable_value.cat3.erb +20 -0
- data/templates/cat3/_measure_data.cat3.erb +126 -0
- data/templates/cat3/_performance_rate.cat3.erb +16 -0
- data/templates/cat3/_supplemental_data.cat3.erb +36 -0
- data/templates/cat3/show.cat3.erb +157 -0
- data/templates/ccda/show.ccda.erb +1 -1
- metadata +25 -45
- data/lib/hqmf-parser/value_sets/value_set_parser.rb +0 -241
@@ -1,36 +1,57 @@
|
|
1
1
|
module HealthDataStandards
|
2
2
|
module CQM
|
3
|
+
class Measure
|
4
|
+
include Mongoid::Document
|
5
|
+
store_in collection: 'measures'
|
6
|
+
field :id, type: String
|
7
|
+
field :sub_id, type: String
|
8
|
+
field :name, type: String
|
9
|
+
field :subtitle, type: String
|
10
|
+
field :short_subtitle, type: String
|
11
|
+
field :hqmf_id, type: String
|
12
|
+
field :hqmf_set_id, type: String
|
13
|
+
field :hqmf_version_number, type: String
|
14
|
+
field :nqf_id, type: String
|
15
|
+
field :type, type: String
|
16
|
+
field :category, type: String
|
17
|
+
field :population_ids , type: Hash
|
18
|
+
field :oids, type: Array
|
19
|
+
field :data_criteria, type: Array
|
3
20
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
field :id, type: String
|
8
|
-
field :sub_id, type: String
|
9
|
-
field :name, type: String
|
10
|
-
field :subtitle, type: String
|
11
|
-
field :short_subtitle, type: String
|
12
|
-
field :hqmf_id, type: String
|
13
|
-
field :hqmf_set_id, type: String
|
14
|
-
field :hqmf_version_number, type: String
|
15
|
-
field :nqf_id, type: String
|
16
|
-
field :type, type: String
|
17
|
-
field :category, type: String
|
18
|
-
field :population_ids , type: Hash
|
19
|
-
field :oids, type: Array
|
20
|
-
field :data_criteria, type: Array
|
21
|
-
scope :top_level_by_type , ->(type){where({"type"=> type}).any_of({"sub_id" => nil}, {"sub_id" => "a"})}
|
22
|
-
scope :top_level , any_of({"sub_id" => nil}, {"sub_id" => "a"})
|
23
|
-
scope :order_by_id_sub_id, order_by([["id", :asc],["sub_id", :asc]])
|
24
|
-
index({oids: 1})
|
25
|
-
index({hqmf_id: 1})
|
26
|
-
index({category: 1})
|
27
|
-
index "bundle_id" => 1
|
28
|
-
|
29
|
-
validates_presence_of :id
|
30
|
-
validates_presence_of :name
|
31
|
-
|
21
|
+
scope :top_level_by_type , ->(type){where({"type"=> type}).any_of({"sub_id" => nil}, {"sub_id" => "a"})}
|
22
|
+
scope :top_level , any_of({"sub_id" => nil}, {"sub_id" => "a"})
|
23
|
+
scope :order_by_id_sub_id, order_by([["id", :asc],["sub_id", :asc]])
|
32
24
|
|
25
|
+
index({oids: 1})
|
26
|
+
index({hqmf_id: 1})
|
27
|
+
index({category: 1})
|
28
|
+
index "bundle_id" => 1
|
29
|
+
|
30
|
+
validates_presence_of :id
|
31
|
+
validates_presence_of :name
|
33
32
|
|
34
|
-
|
35
|
-
|
33
|
+
def self.categories
|
34
|
+
pipeline = []
|
35
|
+
pipeline << {'$group' => {_id: "$id",
|
36
|
+
name: {"$first" => "$name"},
|
37
|
+
description: {"$first" => "$description"},
|
38
|
+
sub_ids: {'$push' => "$sub_id"},
|
39
|
+
subs: {'$push' => {"sub_id" => "$sub_id", "short_subtitle" => "$short_subtitle"}},
|
40
|
+
category: {'$first' => "$category"}}}
|
41
|
+
|
42
|
+
pipeline << {'$group' => {_id: "$category",
|
43
|
+
measures: {'$push' => {"id" => "$_id",
|
44
|
+
'name' => "$name",
|
45
|
+
'description' => "$description",
|
46
|
+
'subs' => "$subs",
|
47
|
+
'sub_ids' => "$sub_ids"
|
48
|
+
}}}}
|
49
|
+
|
50
|
+
pipeline << {'$project' => {'category' => '$_id', 'measures' => 1, '_id' => 0}}
|
51
|
+
|
52
|
+
pipeline << {'$sort' => {"category" => 1}}
|
53
|
+
Mongoid.default_session.command(aggregate: 'measures', pipeline: pipeline)['result']
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
36
57
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module HealthDataStandards
|
2
|
+
module CQM
|
3
|
+
class QueryCache
|
4
|
+
include Mongoid::Document
|
5
|
+
store_in collection: 'query_cache'
|
6
|
+
|
7
|
+
field :measure_id, type: String
|
8
|
+
field :sub_id, type: String
|
9
|
+
field :population_ids, type: Hash
|
10
|
+
field :effective_date, type: Integer
|
11
|
+
field :IPP, type: Integer
|
12
|
+
field :DENOM, type: Integer
|
13
|
+
field :NUMER, type: Integer
|
14
|
+
field :antinumerator, type: Integer
|
15
|
+
field :DENEX, type: Integer
|
16
|
+
field :DENEXCEP, type: Integer
|
17
|
+
field :MSRPOPL, type: Integer
|
18
|
+
field :OBSERV, type: Float
|
19
|
+
field :supplemental_data, type: Hash
|
20
|
+
|
21
|
+
def self.aggregate_measure(measure_id, effective_date, test_id=nil)
|
22
|
+
cache_entries = self.where(effective_date: effective_date, measure_id: measure_id, test_id: test_id)
|
23
|
+
aggregate_count = AggregateCount.new
|
24
|
+
aggregate_count.measure_id = measure_id
|
25
|
+
cache_entries.each do |cache_entry|
|
26
|
+
if cache_entry.is_stratification?
|
27
|
+
stratification = Stratification.new
|
28
|
+
stratification.populations = cache_entry.build_populations
|
29
|
+
stratification.id = cache_entry.population_ids['stratification']
|
30
|
+
aggregate_count.stratifications << stratification
|
31
|
+
else
|
32
|
+
aggregate_count.top_level_populations = cache_entry.build_populations
|
33
|
+
if cache_entry.supplemental_data.present?
|
34
|
+
aggregate_count.supplemental_data = cache_entry.supplemental_data
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
aggregate_count
|
39
|
+
end
|
40
|
+
|
41
|
+
def is_stratification?
|
42
|
+
population_ids.has_key?('stratification')
|
43
|
+
end
|
44
|
+
|
45
|
+
def is_cv?
|
46
|
+
population_ids.has_key?('MSRPOPL')
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_populations
|
50
|
+
populations = []
|
51
|
+
population_ids.each do |population_type, population_id|
|
52
|
+
unless population_type == 'stratification'
|
53
|
+
population = Population.new
|
54
|
+
population.type = population_type
|
55
|
+
population.id = population_id
|
56
|
+
population.value = self[population_type]
|
57
|
+
populations << population
|
58
|
+
end
|
59
|
+
end
|
60
|
+
populations
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
class Record
|
2
2
|
include Mongoid::Document
|
3
|
+
extend Memoist
|
3
4
|
|
4
5
|
field :title, type: String
|
5
6
|
field :first, type: String
|
@@ -30,7 +31,11 @@ class Record
|
|
30
31
|
embeds_many :medications
|
31
32
|
embeds_many :procedures
|
32
33
|
embeds_many :results, class_name: "LabResult"
|
33
|
-
embeds_many :
|
34
|
+
embeds_many :socialhistories, class_name: "Entry"
|
35
|
+
|
36
|
+
alias :social_history :socialhistories
|
37
|
+
alias :social_history= :socialhistories=
|
38
|
+
|
34
39
|
embeds_many :vital_signs
|
35
40
|
embeds_many :support
|
36
41
|
embeds_many :advance_directives, class_name: "Entry"
|
@@ -47,7 +52,7 @@ class Record
|
|
47
52
|
|
48
53
|
scope :by_provider, ->(prov, effective_date) { (effective_date) ? where(provider_queries(prov.id, effective_date)) : where('provider_performances.provider_id'=>prov.id) }
|
49
54
|
scope :by_patient_id, ->(id) { where(:medical_record_number => id) }
|
50
|
-
|
55
|
+
|
51
56
|
def providers
|
52
57
|
provider_performances.map {|pp| pp.provider }
|
53
58
|
end
|
@@ -67,10 +72,30 @@ class Record
|
|
67
72
|
end
|
68
73
|
matching_entries_by_section.flatten
|
69
74
|
end
|
75
|
+
|
76
|
+
memoize :entries_for_oid
|
70
77
|
|
71
78
|
alias :clinical_trial_participant :clinicalTrialParticipant
|
72
79
|
alias :clinical_trial_participant= :clinicalTrialParticipant=
|
73
|
-
|
80
|
+
|
81
|
+
# Removed duplicate entries from a section based on id. This method may
|
82
|
+
# lose information because it does not compare entries based on clinical
|
83
|
+
# content
|
84
|
+
def dedup_section!(section)
|
85
|
+
unique_entries = self.send(section).uniq do |entry|
|
86
|
+
if entry.respond_to?(:cda_identifier) && entry.cda_identifier.present?
|
87
|
+
entry.cda_identifier
|
88
|
+
else
|
89
|
+
entry.id
|
90
|
+
end
|
91
|
+
end
|
92
|
+
self.send("#{section}=", unique_entries)
|
93
|
+
end
|
94
|
+
|
95
|
+
def dedup_record!
|
96
|
+
Record::Sections.each {|section| self.dedup_section!(section)}
|
97
|
+
end
|
98
|
+
|
74
99
|
private
|
75
100
|
|
76
101
|
def self.provider_queries(provider_id, effective_date)
|
@@ -69,6 +69,26 @@ module HealthDataStandards
|
|
69
69
|
code_set_name
|
70
70
|
end
|
71
71
|
end
|
72
|
+
def self.denormalize_code_set_name(code_set_name)
|
73
|
+
case code_set_name
|
74
|
+
when'RxNorm'
|
75
|
+
'RXNORM'
|
76
|
+
when'ICD-9-CM'
|
77
|
+
'ICD9CM'
|
78
|
+
when'ICD-10-CM'
|
79
|
+
'ICD10CM'
|
80
|
+
when'ICD-10-PCS'
|
81
|
+
'ICD10PCS'
|
82
|
+
when'SNOMED-CT'
|
83
|
+
'SNOMEDCT'
|
84
|
+
when'CDC Race'
|
85
|
+
'CDCREC'
|
86
|
+
when'HL7 Healthcare Service Location'
|
87
|
+
'HSLOC'
|
88
|
+
else
|
89
|
+
code_set_name
|
90
|
+
end
|
91
|
+
end
|
72
92
|
end
|
73
93
|
end
|
74
94
|
end
|
@@ -4,26 +4,36 @@ db_name = ENV['DB_NAME'] || 'test'
|
|
4
4
|
|
5
5
|
namespace :bundle do
|
6
6
|
desc 'Import a quality bundle into the database.'
|
7
|
-
task :import, [:bundle_path, :delete_existing, :update_measures, :type] => [:environment] do |task, args|
|
7
|
+
task :import, [:bundle_path, :delete_existing, :update_measures, :type, :create_indexes] => [:environment] do |task, args|
|
8
8
|
raise "The path to the measures zip file must be specified" unless args.bundle_path
|
9
9
|
options = {:delete_existing => (args.delete_existing == "true"),
|
10
|
-
:type => args.type
|
10
|
+
:type => args.type,
|
11
11
|
:update_measures => (args.update_measures == "true")
|
12
12
|
}
|
13
13
|
|
14
14
|
bundle = File.open(args.bundle_path)
|
15
15
|
importer = HealthDataStandards::Import::Bundle::Importer
|
16
16
|
bundle_contents = importer.import(bundle, options)
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
|
18
|
+
counts = {measures: bundle_contents.measures.count,
|
19
|
+
records: bundle_contents.records.count,
|
20
|
+
extensions: bundle_contents[:extensions].count,
|
21
|
+
value_sets: bundle_contents.value_sets.count}
|
22
|
+
|
23
|
+
if (args.create_indexes != 'false')
|
24
|
+
::Rails.application.eager_load!
|
25
|
+
::Rails::Mongoid.create_indexes
|
26
|
+
end
|
20
27
|
|
21
28
|
puts "Successfully imported bundle at: #{args.bundle_path}"
|
22
29
|
puts "\t Imported into environment: #{Rails.env.upcase}" if defined? Rails
|
23
30
|
puts "\t Loaded #{args.type || 'all'} measures"
|
24
|
-
puts "\t Measures Loaded: #{
|
25
|
-
puts "\t Test Patients Loaded: #{
|
26
|
-
puts "\t Extensions Loaded: #{
|
31
|
+
puts "\t Sub-Measures Loaded: #{counts[:measures]}"
|
32
|
+
puts "\t Test Patients Loaded: #{counts[:records]}"
|
33
|
+
puts "\t Extensions Loaded: #{counts[:extensions]}"
|
34
|
+
puts "\t Value Sets Loaded: #{counts[:value_sets]}"
|
35
|
+
|
36
|
+
|
27
37
|
end
|
28
38
|
|
29
39
|
# this task is most likely temporary. Once Bonnie can handle both EP and EH measures together, this would no longer be required.
|
@@ -16,10 +16,14 @@ module HealthDataStandards
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.template_id_by_definition_and_status(definition, status, negation=false)
|
19
|
-
|
19
|
+
kv_pair = template_id_map.find {|k, v| v['definition'] == definition &&
|
20
20
|
v['status'] == status &&
|
21
21
|
v['negation'] == negation}
|
22
|
-
|
22
|
+
if kv_pair
|
23
|
+
kv_pair.first
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
23
27
|
end
|
24
28
|
end
|
25
29
|
end
|
@@ -223,6 +223,10 @@
|
|
223
223
|
"definition":"medication",
|
224
224
|
"status":"discharge",
|
225
225
|
"negation":false},
|
226
|
+
"2.16.840.1.113883.3.560.1.200":{
|
227
|
+
"definition":"medication",
|
228
|
+
"status":"discharge",
|
229
|
+
"negation":true},
|
226
230
|
"2.16.840.1.113883.3.560.1.13":{
|
227
231
|
"definition":"medication",
|
228
232
|
"status":"active",
|
@@ -3,24 +3,27 @@ require 'uri'
|
|
3
3
|
module HealthDataStandards
|
4
4
|
module Util
|
5
5
|
class VSApi
|
6
|
-
attr_accessor :api_url, :ticket_url,
|
6
|
+
attr_accessor :api_url, :ticket_url, :username, :password
|
7
7
|
|
8
|
-
def initialize(ticket_url, api_url,username,password)
|
8
|
+
def initialize(ticket_url, api_url, username, password)
|
9
9
|
@api_url = api_url
|
10
10
|
@ticket_url = ticket_url
|
11
11
|
@username = username
|
12
12
|
@password = password
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
|
16
|
+
def get_valueset(oid, effective_date=nil, &block)
|
17
|
+
params = {id: oid, ticket: get_ticket}
|
18
|
+
params[:effectiveDate] = effective_date if effective_date
|
19
|
+
vs = RestClient.get api_url, {:params=>params}
|
17
20
|
yield oid,vs if block_given?
|
18
21
|
vs
|
19
22
|
end
|
20
23
|
|
21
|
-
def process_valuesets(oids, &block)
|
24
|
+
def process_valuesets(oids, effective_date=nil, &block)
|
22
25
|
oids.each do |oid|
|
23
|
-
vs = get_valueset(oid)
|
26
|
+
vs = get_valueset(oid,effective_date)
|
24
27
|
yield oid,vs
|
25
28
|
end
|
26
29
|
end
|
@@ -483,16 +483,16 @@
|
|
483
483
|
"qds_data_type":"medication_administered",
|
484
484
|
"not_supported":false},
|
485
485
|
"medication_discharge": {
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
486
|
+
"title":"medication, discharge",
|
487
|
+
"category":"medications",
|
488
|
+
"definition":"medication_discharge",
|
489
|
+
"status":"discharge",
|
490
|
+
"sub_category":"discharge",
|
491
|
+
"hard_status":true,
|
492
|
+
"patient_api_function":"allMedications",
|
493
|
+
"standard_category":"medication",
|
494
|
+
"qds_data_type":"medication_discharge",
|
495
|
+
"not_supported":false},
|
496
496
|
"medication_adverse_effects":{
|
497
497
|
"title":"medication, adverse effects",
|
498
498
|
"category":"medications",
|
data/lib/hqmf-parser.rb
CHANGED
@@ -49,8 +49,6 @@ require_relative 'hqmf-parser/converter/pass1/simple_population_criteria'
|
|
49
49
|
require_relative 'hqmf-parser/converter/pass2/comparison_converter'
|
50
50
|
require_relative 'hqmf-parser/converter/pass2/operator_converter'
|
51
51
|
|
52
|
-
require_relative 'hqmf-parser/value_sets/value_set_parser'
|
53
|
-
|
54
52
|
require_relative 'hqmf-parser/parser'
|
55
53
|
|
56
54
|
require_relative 'hqmf-generator/hqmf-generator'
|
@@ -4,8 +4,20 @@ module HQMF1
|
|
4
4
|
class Value
|
5
5
|
include HQMF1::Utilities
|
6
6
|
|
7
|
-
def initialize(entry)
|
7
|
+
def initialize(entry, inclusive=nil)
|
8
8
|
@entry = entry
|
9
|
+
|
10
|
+
if (inclusive.nil?)
|
11
|
+
case attr_val('./@inclusive')
|
12
|
+
when 'true'
|
13
|
+
@inclusive = true
|
14
|
+
else
|
15
|
+
@inclusive = false
|
16
|
+
end
|
17
|
+
else
|
18
|
+
@inclusive = inclusive
|
19
|
+
end
|
20
|
+
|
9
21
|
end
|
10
22
|
|
11
23
|
def value
|
@@ -17,12 +29,7 @@ module HQMF1
|
|
17
29
|
end
|
18
30
|
|
19
31
|
def inclusive?
|
20
|
-
|
21
|
-
when 'true'
|
22
|
-
true
|
23
|
-
else
|
24
|
-
false
|
25
|
-
end
|
32
|
+
@inclusive
|
26
33
|
end
|
27
34
|
|
28
35
|
def to_json
|
@@ -40,6 +47,11 @@ module HQMF1
|
|
40
47
|
if @entry
|
41
48
|
@low = optional_value('./cda:low')
|
42
49
|
@high = optional_value('./cda:high')
|
50
|
+
if (@low == nil && @high == nil)
|
51
|
+
@low = optional_value('.',true)
|
52
|
+
@high = optional_value('.',true)
|
53
|
+
puts "\tfound = relationship parsing temporal reference (bugfix)"
|
54
|
+
end
|
43
55
|
end
|
44
56
|
end
|
45
57
|
|
@@ -52,10 +64,10 @@ module HQMF1
|
|
52
64
|
|
53
65
|
private
|
54
66
|
|
55
|
-
def optional_value(xpath)
|
67
|
+
def optional_value(xpath, inclusive=nil)
|
56
68
|
value_def = @entry.at_xpath(xpath)
|
57
69
|
if value_def
|
58
|
-
Value.new(value_def)
|
70
|
+
Value.new(value_def, inclusive)
|
59
71
|
else
|
60
72
|
nil
|
61
73
|
end
|