health-data-standards 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. data/lib/health-data-standards.rb +20 -1
  2. data/lib/health-data-standards/export/ccr.rb +30 -5
  3. data/lib/health-data-standards/export/green_c32/entry.rb +15 -0
  4. data/lib/health-data-standards/export/green_c32/export_generator.rb +23 -0
  5. data/lib/health-data-standards/export/template_helper.rb +0 -1
  6. data/lib/health-data-standards/import/c32/condition_importer.rb +66 -0
  7. data/lib/health-data-standards/import/c32/provider_importer.rb +66 -0
  8. data/lib/health-data-standards/import/c32/section_importer.rb +2 -0
  9. data/lib/health-data-standards/import/ccr/patient_importer.rb +208 -0
  10. data/lib/health-data-standards/import/ccr/product_importer.rb +60 -0
  11. data/lib/health-data-standards/import/ccr/provider_importer.rb +62 -0
  12. data/lib/health-data-standards/import/ccr/result_importer.rb +49 -0
  13. data/lib/health-data-standards/import/ccr/section_importer.rb +124 -0
  14. data/lib/health-data-standards/import/ccr/simple_importer.rb +30 -0
  15. data/lib/health-data-standards/import/green_c32/condition_importer.rb +45 -0
  16. data/lib/health-data-standards/import/green_c32/patient_importer.rb +14 -0
  17. data/lib/health-data-standards/import/green_c32/result_importer.rb +30 -0
  18. data/lib/health-data-standards/import/green_c32/section_importer.rb +93 -0
  19. data/lib/health-data-standards/models/comment.rb +2 -0
  20. data/lib/health-data-standards/models/condition.rb +10 -0
  21. data/lib/health-data-standards/models/entry.rb +5 -0
  22. data/lib/health-data-standards/models/fulfillment_history.rb +2 -0
  23. data/lib/health-data-standards/models/lab_result.rb +1 -0
  24. data/lib/health-data-standards/models/provider.rb +51 -0
  25. data/lib/health-data-standards/models/provider_performance.rb +10 -0
  26. data/lib/health-data-standards/models/record.rb +21 -4
  27. data/lib/health-data-standards/models/treating_provider.rb +3 -0
  28. data/lib/health-data-standards/util/hl7_helper.rb +1 -0
  29. data/templates/_condition.gc32.erb +11 -0
  30. data/templates/_result.gc32.erb +20 -0
  31. data/templates/show.c32.erb +16 -5
  32. metadata +33 -12
@@ -20,9 +20,14 @@ require_relative 'health-data-standards/export/c32'
20
20
  require_relative 'health-data-standards/export/ccr'
21
21
  require_relative 'health-data-standards/export/csv'
22
22
 
23
+ require_relative 'health-data-standards/export/green_c32/entry'
24
+ require_relative 'health-data-standards/export/green_c32/export_generator'
25
+
26
+
23
27
  require_relative 'health-data-standards/models/entry'
24
28
  require_relative 'health-data-standards/models/allergy'
25
29
  require_relative 'health-data-standards/models/encounter'
30
+ require_relative 'health-data-standards/models/condition'
26
31
  require_relative 'health-data-standards/models/immunization'
27
32
  require_relative 'health-data-standards/models/fulfillment_history'
28
33
  require_relative 'health-data-standards/models/order_information'
@@ -30,6 +35,8 @@ require_relative 'health-data-standards/models/medication'
30
35
  require_relative 'health-data-standards/models/procedure'
31
36
  require_relative 'health-data-standards/models/lab_result'
32
37
  require_relative 'health-data-standards/models/record'
38
+ require_relative 'health-data-standards/models/provider'
39
+ require_relative 'health-data-standards/models/provider_performance'
33
40
 
34
41
  require_relative 'health-data-standards/import/c32/section_importer'
35
42
  require_relative 'health-data-standards/import/c32/allergy_importer'
@@ -39,4 +46,16 @@ require_relative 'health-data-standards/import/c32/medication_importer'
39
46
  require_relative 'health-data-standards/import/c32/procedure_importer'
40
47
  require_relative 'health-data-standards/import/c32/result_importer'
41
48
  require_relative 'health-data-standards/import/c32/vital_sign_importer'
42
- require_relative 'health-data-standards/import/c32/patient_importer'
49
+ require_relative 'health-data-standards/import/c32/patient_importer'
50
+ require_relative 'health-data-standards/import/c32/provider_importer'
51
+
52
+ require_relative 'health-data-standards/import/ccr/patient_importer'
53
+ require_relative 'health-data-standards/import/ccr/provider_importer'
54
+ require_relative 'health-data-standards/import/ccr/section_importer'
55
+ require_relative 'health-data-standards/import/ccr/result_importer'
56
+ require_relative 'health-data-standards/import/ccr/simple_importer'
57
+ require_relative 'health-data-standards/import/ccr/product_importer'
58
+
59
+ require_relative 'health-data-standards/import/green_c32/section_importer'
60
+ require_relative 'health-data-standards/import/green_c32/result_importer'
61
+ require_relative 'health-data-standards/import/green_c32/condition_importer'
@@ -36,6 +36,7 @@ module HealthDataStandards
36
36
  to_ccr_results(xml, patient)
37
37
  to_ccr_procedures(xml, patient)
38
38
  to_ccr_encounters(xml, patient)
39
+ to_ccr_socialhistory(xml, patient)
39
40
 
40
41
  end
41
42
  to_ccr_actors(xml, patient)
@@ -169,14 +170,15 @@ module HealthDataStandards
169
170
  #time
170
171
  xml.ExactDateTime(convert_to_ccr_time_string(res.time))
171
172
  end
172
- xml.Description do
173
- xml.Text(res.description)
174
- code_section(xml, res.codes)
175
- end
176
-
173
+
177
174
  xml.Source
178
175
  xml.Test do
179
176
  xml.CCRDataObjectID("#{ccr_id}TestResult")
177
+ xml.Description do
178
+ xml.Text(res.description)
179
+ code_section(xml, res.codes)
180
+ end
181
+
180
182
  xml.Source
181
183
  xml.TestResult do
182
184
  xml.Value(res.value["scalar"])
@@ -320,6 +322,29 @@ module HealthDataStandards
320
322
  end
321
323
  end
322
324
  end
325
+
326
+ # Builds the XML snippet for the social history section inside the CCR standard
327
+ #
328
+ # @return [Builder::XmlMarkup] CCR XML representation of patient data
329
+ def to_ccr_socialhistory(xml, patient)
330
+ if patient.social_history.present?
331
+ xml.SocialHistory do
332
+ patient.social_history.each_with_index do |history, index|
333
+ xml.SocialHistoryElement do
334
+ xml.CCRDataObjectID("SH000#{index + 1}")
335
+
336
+
337
+ xml.Description do
338
+ xml.Text(history.description)
339
+ code_section(xml, history.codes)
340
+ end
341
+
342
+
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end
323
348
 
324
349
  # Builds the XML snippet for a actors section inside the CCR standard
325
350
  #
@@ -0,0 +1,15 @@
1
+ module HealthDataStandards
2
+ module Export
3
+ module GreenC32
4
+ module Entry
5
+ include TemplateHelper
6
+
7
+ def export(object, object_type)
8
+ self.template_format = "gc32"
9
+ render(partial: object_type, locals: {object_type => object})
10
+ end
11
+ extend self
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module HealthDataStandards
2
+ module Export
3
+ module GreenC32
4
+ # Module that will create objects that can be used to export GreenCDA sections
5
+ module ExportGenerator
6
+ # Creates an object that can be used to export objects into GreenCDA.
7
+ # @example Creating an results exporter
8
+ # exporter = ExportGenerator.create_exporter_for(:result)
9
+ # exporter.export(result) # => Returns GreenCDA XML in a String
10
+ # @param [Symbol] section the section to create the exporter for
11
+ # @return [Object] that has an export method
12
+ def create_exporter_for(section)
13
+ object = Object.new
14
+ object.define_singleton_method(:export) do |section_instance|
15
+ HealthDataStandards::Export::GreenC32::Entry.export(section_instance, section)
16
+ end
17
+ object
18
+ end
19
+ extend self
20
+ end
21
+ end
22
+ end
23
+ end
@@ -16,7 +16,6 @@ module HealthDataStandards
16
16
  end
17
17
 
18
18
  def render(params)
19
- #binding.pry
20
19
  erb = nil
21
20
  if params[:template]
22
21
  erb = template(params[:template])
@@ -0,0 +1,66 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module C32
4
+ class ConditionImporter < SectionImporter
5
+
6
+ def initialize
7
+ @entry_xpath = "//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.11']"
8
+ @code_xpath = "./cda:code"
9
+ @description_xpath = "./cda:code/cda:originalText/cda:reference[@value] | ./cda:text/cda:reference[@value] "
10
+ end
11
+
12
+ def create_entries(doc, id_map = {})
13
+ @id_map = id_map
14
+ condition_list = []
15
+ entry_elements = doc.xpath(@entry_xpath)
16
+
17
+ entry_elements.each do |entry_element|
18
+ condition = Condition.new
19
+
20
+ extract_codes(entry_element, condition)
21
+ extract_dates(entry_element, condition)
22
+ extract_description(entry_element, condition, id_map)
23
+
24
+ condition.diagnosis_priority = extract_code(entry_element, "???")
25
+
26
+ condition.problem_date = extract_code(entry_element,
27
+ "./cda:entryRelationship[@typeCode='SUBJ']/cda:observation[@classCode='OBS']/cda.effectiveTime")
28
+
29
+ condition.problem_type = extract_code(entry_element,
30
+ "./cda:entryRelationship[@typeCode='SUBJ']/cda:observation[@classCode='OBS']")
31
+
32
+ condition.problem_name = extract_code(entry_element, "./cda:text")
33
+
34
+ condition.problem_code = extract_code(entry_element,
35
+ "./cda:entryRelationship[@typeCode='SUBJ']/cda:observation[@classCode='OBS']/cda:code[@code='11450-4']/cda:value[@codeSystem='2.16.840.1.113883.96']")
36
+
37
+ condition.age_at_onset = extract_code(entry_element, "???")
38
+
39
+ extract_cause_of_death(entry_element, condition)
40
+
41
+ condition.problem_status = extract_code(entry_element,
42
+ "./cda:entryRelationship[@typeCode='SUBJ']/cda:observation[@classCode='OBS']/cda:statusCode[@code='completed']")
43
+
44
+ condition_list << condition
45
+ end
46
+
47
+ condition_list
48
+ end
49
+
50
+ private
51
+
52
+ def extract_cause_of_death(parent_element, condition)
53
+ cause_of_death_element = parent_element.at_xpath("???")
54
+
55
+ if cause_of_death_element
56
+ condition.cause_of_death = {}
57
+
58
+ condition.cause_of_death[:time_of_death] = cause_of_death_element.xpath("???")
59
+ condition.cause_of_death[:age_at_death] = cause_of_death_element.xpath("???")
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,66 @@
1
+ require "date"
2
+ require "date/delta"
3
+
4
+ module HealthDataStandards
5
+ module Import
6
+ module C32
7
+ class ProviderImporter
8
+ include Singleton
9
+
10
+ # Extract Healthcare Providers from C32
11
+ #
12
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
13
+ # will have the "cda" namespace registered to "urn:hl7-org:v3"
14
+ # @return [Array] an array of providers found in the document
15
+ def extract_providers(doc, use_encounters=false)
16
+ xpath_base = use_encounters ? "//cda:encounter/cda:performer" : "//cda:documentationOf/cda:serviceEvent/cda:performer"
17
+
18
+ performers = doc.xpath(xpath_base)
19
+
20
+ providers = performers.map do |performer|
21
+ provider = {}
22
+ entity = performer.xpath(performer, "./cda:assignedEntity")
23
+ name = entity.xpath("./cda:assignedPerson/cda:name")
24
+ provider[:title] = extract_data(name, "./cda:prefix")
25
+ provider[:given_name] = extract_data(name, "./cda:given[1]")
26
+ provider[:family_name] = extract_data(name, "./cda:family")
27
+ provider[:phone] = extract_data(entity, "./cda:telecom/@value") { |text| text.gsub("tel:", "") }
28
+ provider[:organization] = extract_data(entity, "./cda:representedOrganization/cda:name")
29
+ provider[:specialty] = extract_data(entity, "./cda:code/@code")
30
+ time = performer.xpath(performer, "./cda:time")
31
+ provider[:start] = extract_date(time, "./cda:low/@value")
32
+ provider[:end] = extract_date(time, "./cda:high/@value")
33
+ # NIST sample C32s use different OID for NPI vs C83, support both
34
+ npi = extract_data(entity, "./cda:id[@root='2.16.840.1.113883.4.6' or @root='2.16.840.1.113883.3.72.5.2']/@extension")
35
+ if Provider.valid_npi?(npi)
36
+ provider[:npi] = npi
37
+ else
38
+ puts "Warning: Invalid NPI (#{npi})"
39
+ end
40
+ provider
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def extract_date(subject,query)
47
+ date = extract_data(subject,query)
48
+ date ? Date.parse(date).to_time.to_i : nil
49
+ end
50
+
51
+ # Returns nil if result is an empty string, block allows text munging of result if there is one
52
+ def extract_data(subject, query)
53
+ result = subject.xpath(query).text
54
+ if result == ""
55
+ nil
56
+ elsif block_given?
57
+ yield(result)
58
+ else
59
+ result
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
@@ -136,6 +136,8 @@ module HealthDataStandards
136
136
 
137
137
  def import_actor(actor_element)
138
138
  actor_hash = {}
139
+ npi = actor_element.xpath("./cda:assignedEntity/cda:id[@root='2.16.840.1.113883.4.6' or @root='2.16.840.1.113883.3.72.5.2']/@extension")
140
+ actor_hash[:npi] = npi if npi
139
141
  addresses = actor_element.xpath("./cda:assignedEntity/cda:addr").try(:map) {|ae| import_address(ae)}
140
142
  telecoms = actor_element.xpath("./cda:assignedEntity/cda:telecom").try(:map) {|te| import_telecom(te)}
141
143
  person_element = actor_element.at_xpath("./cda:assignedEntity/cda:assignedPerson")
@@ -0,0 +1,208 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module CCR
4
+ # This class is the central location for taking an ASTM CCR XML document and converting it
5
+ # into the processed form we store in MongoDB. The class does this by running each measure
6
+ # independently on the XML document
7
+ #
8
+ # This class is a Singleton. It should be accessed by calling PatientImporter.instance
9
+ class PatientImporter
10
+
11
+ include Singleton
12
+
13
+ Gender = {"male" => "M", "female" => "F"}
14
+
15
+ # Creates a new PatientImporter with the following XPath expressions used to find content in
16
+ # an ASTM CCR
17
+ #
18
+ # Encounter entries
19
+ # //ccr:Encounters/ccr:Encounter
20
+ # Procedure entries
21
+ # //ccr:Procedures/ccr:Procedure
22
+ #
23
+ # Result entries -
24
+ # //ccr:Results/ccr:Result
25
+ #
26
+ # Vital sign entries
27
+ # //ccr:VitalSigns/ccr:Result
28
+ #
29
+ # Medication entries
30
+ # //ccr:Medications/ccr:Medication
31
+ #
32
+ # Codes for medications are found in the Product sections
33
+ # ./ccr:Product
34
+ #
35
+ # Condition entries
36
+ # //ccr:Problems/ccr:Problem
37
+ #
38
+ # Social History entries
39
+ # //ccr:SocialHistory/ccr:SocialHistoryElement
40
+ #
41
+ # Care Goal entries
42
+ # //ccr:Goals/ccr:Goal
43
+ #
44
+ # Allergy entries
45
+ # //ccr:Alerts/ccr:Alert
46
+ #
47
+ # Immunization entries
48
+ # //ccr:Immunizations/ccr:Immunization
49
+ #
50
+ # Codes for immunizations are found in the substanceAdministration with the following relative XPath
51
+ # ./ccr:Product
52
+
53
+ def initialize (check_usable = true)
54
+ @measure_importers = {}
55
+ @section_importers = {}
56
+ @section_importers[:encounters] = SimpleImporter.new("//ccr:Encounters/ccr:Encounter",:encounters)
57
+ @section_importers[:procedures] = SimpleImporter.new("//ccr:Procedures/ccr:Procedure",:procedures)
58
+ @section_importers[:results] = ResultImporter.new("//ccr:Results/ccr:Result",:results)
59
+ @section_importers[:vital_signs] = ResultImporter.new("//ccr:VitalSigns/ccr:Result",:vital_signs)
60
+ @section_importers[:medications] = ProductImporter.new("//ccr:Medications/ccr:Medication", :medications)
61
+ @section_importers[:conditions] = SimpleImporter.new("//ccr:Problems/ccr:Problem",:conditions)
62
+ @section_importers[:social_history] = SimpleImporter.new("//ccr:SocialHistory/ccr:SocialHistoryElement", :social_history)
63
+ @section_importers[:care_goals] = SimpleImporter.new("//ccr:Goals/ccr:Goal",:care_goals)
64
+ @section_importers[:medical_equipment] = ProductImporter.new("//ccr:Equpment/ccr:EquipmentElement",:medical_equipment)
65
+ @section_importers[:allergies] = SimpleImporter.new("//ccr:Alerts/ccr:Alert",:allergies)
66
+ @section_importers[:immunizations] = ProductImporter.new("//ccr:Immunizations/ccr:Immunization",:immunizations)
67
+ end
68
+
69
+
70
+ # @param [boolean] value for check_usable_entries...importer uses true, stats uses false
71
+ def check_usable(check_usable_entries)
72
+ @section_importers.each_pair do |section, importer|
73
+ importer.check_for_usable = check_usable_entries
74
+ end
75
+ end
76
+
77
+ # Parses a ASTM CCR document and returns a Hash of of the patient.
78
+ #
79
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
80
+ # will have the "ccr" namespace registered to ""urn:astm-org:CCR""
81
+ # @return [Hash] a representation of the patient that can be inserted into MongoDB
82
+ def parse_ccr(doc)
83
+ ccr_patient = {}
84
+ entries = create_hash(doc)
85
+ get_demographics(ccr_patient, doc)
86
+ process_events(ccr_patient, entries)
87
+ Record.new(ccr_patient)
88
+ end
89
+ #
90
+ # # Parses a patient hash containing demographic and event information
91
+ # #
92
+ # # @param [Hash] patient_hash patient data
93
+ # # @return [Hash] a representation of the patient that can be inserted into MongoDB
94
+ # def parse_hash(patient_hash)
95
+ # patient_record = {}
96
+ # patient_record['first'] = patient_hash['first']
97
+ # patient_record['patient_id'] = patient_hash['patient_id']
98
+ # patient_record['last'] = patient_hash['last']
99
+ # patient_record['gender'] = patient_hash['gender']
100
+ # patient_record['patient_id'] = patient_hash['patient_id']
101
+ # patient_record['birthdate'] = patient_hash['birthdate']
102
+ # patient_record['race'] = patient_hash['race']
103
+ # patient_record['ethnicity'] = patient_hash['ethnicity']
104
+ # patient_record['languages'] = patient_hash['languages']
105
+ # patient_record['addresses'] = patient_hash['addresses']
106
+ # event_hash = {}
107
+ # patient_hash['events'].each do |key, value|
108
+ # event_hash[key.intern] = parse_events(value)
109
+ # end
110
+ # process_events(patient_record, event_hash)
111
+ # end
112
+
113
+ # Adds the entries and denormalized measure information to the patient_record.
114
+ # Each Entry will be converted to a Hash and stored in an Array under the appropriate
115
+ # section key, such as medications. Measure information is listed under the measures
116
+ # key which has a Hash value. The Hash has the measure id as a key, and the denormalized
117
+ # measure information as a value
118
+ #
119
+ # @param patient_record - Hash with basic patient demographic information
120
+ # @entries - Hash of entries with section names a keys and an Array of Entry values
121
+ def process_events(patient_record, entries)
122
+ patient_record['measures'] = {}
123
+ @measure_importers.each_pair do |measure_id, importer|
124
+ patient_record['measures'][measure_id] = importer.parse(entries)
125
+ end
126
+
127
+
128
+ entries.each_pair do |key, value|
129
+ patient_record[key] = value.map do |e|
130
+ if e.usable?
131
+ e.to_hash
132
+ else
133
+ nil
134
+ end
135
+ end.compact
136
+ end
137
+ patient_record
138
+ end
139
+
140
+ # # Parses a list of event hashes into an array of Entry objects
141
+ # #
142
+ # # @param [Array] event_list list of event hashes
143
+ # # @return [Array] array of Entry objects
144
+ # def parse_events(event_list)
145
+ # event_list.collect do |event|
146
+ # if event.class==String.class
147
+ # # skip String elements in the event list, patient randomization templates
148
+ # # introduce String elements to simplify tailing-comma handling when generating
149
+ # # JSON using ERb
150
+ # nil
151
+ # else
152
+ # QME::Importer::Entry.from_event_hash(event)
153
+ # end
154
+ # end.compact
155
+ # end
156
+
157
+ # Adds a measure to run on a CCR that is passed in
158
+ #
159
+ # @param [MeasureBase] measure an Class that can extract information from a CCR that is necessary
160
+ # to calculate the measure
161
+ def add_measure(measure_id, importer)
162
+ @measure_importers[measure_id] = importer
163
+ end
164
+
165
+ # Create a simple representation of the patient from an ASTM CCR
166
+ #
167
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
168
+ # will have the "ccr" namespace registered to ""urn:astm-org:CCR""
169
+ # @return [Hash] a represnetation of the patient with symbols as keys for each section
170
+ def create_hash(doc, check_usable_entries = false)
171
+ ccr_patient = {}
172
+ @section_importers.each_pair do |section, importer|
173
+ importer.check_for_usable = check_usable_entries
174
+ ccr_patient[section] = importer.create_entries(doc)
175
+ end
176
+ ccr_patient
177
+ end
178
+
179
+ # Inspects a CCR document and populates the patient Hash with first name, last name
180
+ # birth date and gender.
181
+ #
182
+ # @param [Hash] patient A hash that is used to represent the patient
183
+ # @param [Nokogiri::XML::Node] doc The CCR document parsed by Nokogiri
184
+ def get_demographics(patient, doc)
185
+ patientID = doc.at_xpath('//ccr:ContinuityOfCareRecord/ccr:Patient/ccr:ActorID').content
186
+ patientActor = doc.at_xpath("//ccr:ContinuityOfCareRecord/ccr:Actors/ccr:Actor[ccr:ActorObjectID = \"#{patientID}\"]")
187
+ patient['first'] = patientActor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Given').content
188
+ patient['last'] = patientActor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Family').content
189
+ birthdate = patientActor.at_xpath('./ccr:Person//ccr:DateOfBirth/ccr:ExactDateTime | ./ccr:Person//ccr:DateOfBirth/ccr:ApproximateDateTime')
190
+ patient['birthdate'] = Time.iso8601(birthdate).to_i
191
+
192
+ gender_string = patientActor.at_xpath('./ccr:Person/ccr:Gender/ccr:Text').content.downcase
193
+ patient['gender'] = Gender[gender_string.downcase]
194
+
195
+ #race_node = doc.at_xpath('/ccr:placeholder') #how do you find this?
196
+ patient['race'] = nil
197
+ #ethnicity_node = doc.at_xpath()
198
+ patient['ethnicity'] = nil
199
+
200
+ # languages = doc.at_xpath()
201
+ patient['languages'] = nil
202
+
203
+ patient['medical_record_number'] = patientID
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end