health-data-standards 0.3.0 → 0.5.0

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 (42) hide show
  1. data/Gemfile +5 -2
  2. data/Rakefile +6 -1
  3. data/VERSION +1 -1
  4. data/lib/health-data-standards.rb +27 -2
  5. data/lib/health-data-standards/export/view_helper.rb +15 -16
  6. data/lib/health-data-standards/ext/string.rb +5 -0
  7. data/lib/health-data-standards/ext/symbol.rb +8 -0
  8. data/lib/health-data-standards/import/c32/allergy_importer.rb +42 -0
  9. data/lib/health-data-standards/import/c32/encounter_importer.rb +80 -0
  10. data/lib/health-data-standards/import/c32/immunization_importer.rb +61 -0
  11. data/lib/health-data-standards/import/c32/medication_importer.rb +138 -0
  12. data/lib/health-data-standards/import/c32/patient_importer.rb +139 -0
  13. data/lib/health-data-standards/import/c32/procedure_importer.rb +55 -0
  14. data/lib/health-data-standards/import/c32/result_importer.rb +58 -0
  15. data/lib/health-data-standards/import/c32/section_importer.rb +214 -0
  16. data/lib/health-data-standards/import/c32/vital_sign_importer.rb +12 -0
  17. data/lib/health-data-standards/models/allergy.rb +4 -0
  18. data/lib/health-data-standards/models/encounter.rb +7 -0
  19. data/lib/health-data-standards/models/entry.rb +131 -3
  20. data/lib/health-data-standards/models/fulfillment_history.rb +11 -0
  21. data/lib/health-data-standards/models/immunization.rb +5 -0
  22. data/lib/health-data-standards/models/lab_result.rb +4 -0
  23. data/lib/health-data-standards/models/medication.rb +25 -0
  24. data/lib/health-data-standards/models/order_information.rb +9 -0
  25. data/lib/health-data-standards/models/procedure.rb +4 -0
  26. data/lib/health-data-standards/models/record.rb +1 -0
  27. data/lib/health-data-standards/util/code_system_helper.rb +41 -0
  28. data/lib/health-data-standards/util/hl7_helper.rb +25 -0
  29. data/templates/_allergies.c32.erb +3 -1
  30. data/templates/_care_goals.c32.erb +1 -1
  31. data/templates/_code_with_reference.c32.erb +10 -5
  32. data/templates/_conditions.c32.erb +2 -2
  33. data/templates/_encounters.c32.erb +2 -1
  34. data/templates/_immunizations.c32.erb +2 -1
  35. data/templates/_medical_equipment.c32.erb +1 -1
  36. data/templates/_medications.c32.erb +2 -1
  37. data/templates/_narrative_block.c32.erb +14 -0
  38. data/templates/_procedures.c32.erb +2 -1
  39. data/templates/_results.c32.erb +2 -2
  40. data/templates/_social_history.c32.erb +1 -1
  41. data/templates/_vital_signs.c32.erb +2 -2
  42. metadata +37 -16
@@ -0,0 +1,139 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module C32
4
+
5
+ # This class is the central location for taking a HITSP C32 XML document and converting it
6
+ # into the processed form we store in MongoDB. The class does this by running each measure
7
+ # independently on the XML document
8
+ #
9
+ # This class is a Singleton. It should be accessed by calling PatientImporter.instance
10
+ class PatientImporter
11
+
12
+ include Singleton
13
+ include HealthDataStandards::Util
14
+
15
+ # Creates a new PatientImporter with the following XPath expressions used to find content in
16
+ # a HITSP C32:
17
+ #
18
+ # Encounter entries
19
+ # //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.127']/cda:entry/cda:encounter
20
+ #
21
+ # Procedure entries
22
+ # //cda:procedure[cda:templateId/@root='2.16.840.1.113883.10.20.1.29']
23
+ #
24
+ # Result entries - There seems to be some confusion around the correct templateId, so the code checks for both
25
+ # //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15.1'] | //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']
26
+ #
27
+ # Vital sign entries
28
+ # //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.14']
29
+ #
30
+ # Medication entries
31
+ # //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.112']/cda:entry/cda:substanceAdministration
32
+ #
33
+ # Codes for medications are found in the substanceAdministration with the following relative XPath
34
+ # ./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code
35
+ #
36
+ # Condition entries
37
+ # //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.103']/cda:entry/cda:act/cda:entryRelationship/cda:observation
38
+ #
39
+ # Codes for conditions are determined by examining the value child element as opposed to the code child element
40
+ #
41
+ # Social History entries (non-C32 section, specified in the HL7 CCD)
42
+ # //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']
43
+ #
44
+ # Care Goal entries(non-C32 section, specified in the HL7 CCD)
45
+ # //cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']
46
+ #
47
+ # Allergy entries
48
+ # //cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.18']
49
+ #
50
+ # Immunization entries
51
+ # //cda:substanceAdministration[cda:templateId/@root='2.16.840.1.113883.10.20.1.24']
52
+ #
53
+ # Codes for immunizations are found in the substanceAdministration with the following relative XPath
54
+ # ./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code
55
+ def initialize(check_usable = true)
56
+ @section_importers = {}
57
+ @section_importers[:encounters] = EncounterImporter.new
58
+ @section_importers[:procedures] = ProcedureImporter.new
59
+ @section_importers[:results] = ResultImporter.new
60
+ @section_importers[:vital_signs] = VitalSignImporter.new
61
+ @section_importers[:medications] = MedicationImporter.new
62
+ @section_importers[:conditions] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.103']/cda:entry/cda:act/cda:entryRelationship/cda:observation",
63
+ "./cda:value",
64
+ "./cda:entryRelationship/cda:observation[cda:templateId/@root='2.16.840.1.1 13883.10.20.1.50']/cda:value",
65
+ "./cda:text/cda:reference[@value]")
66
+ @section_importers[:social_history] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']")
67
+ @section_importers[:care_goals] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']")
68
+ @section_importers[:medical_equipment] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.128']/cda:entry/cda:supply",
69
+ "./cda:participant/cda:participantRole/cda:playingDevice/cda:code")
70
+ @section_importers[:allergies] = AllergyImporter.new
71
+ @section_importers[:immunizations] = ImmunizationImporter.new
72
+ end
73
+
74
+ def build_id_map(doc)
75
+ id_map = {}
76
+ path = "//*[@ID]"
77
+ ids = doc.xpath(path)
78
+ ids.each do |id|
79
+ tag = id['ID']
80
+ value = id.content
81
+ id_map[tag] = value
82
+ end
83
+
84
+ id_map
85
+ end
86
+
87
+ # @param [boolean] value for check_usable_entries...importer uses true, stats uses false
88
+ def check_usable(check_usable_entries)
89
+ @section_importers.each_pair do |section, importer|
90
+ importer.check_for_usable = check_usable_entries
91
+ end
92
+ end
93
+
94
+ # Parses a HITSP C32 document and returns a Hash of of the patient.
95
+ #
96
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
97
+ # will have the "cda" namespace registered to "urn:hl7-org:v3"
98
+ # @return [Record] a Mongoid model representing the patient
99
+ def parse_c32(doc)
100
+ c32_patient = Record.new
101
+ get_demographics(c32_patient, doc)
102
+ create_c32_hash(c32_patient, doc)
103
+
104
+ c32_patient
105
+ end
106
+
107
+ # Create a simple representation of the patient from a HITSP C32
108
+ # @param [Record] record Mongoid model to append the Entry objects to
109
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
110
+ # will have the "cda" namespace registered to "urn:hl7-org:v3"
111
+ # @return [Hash] a represnetation of the patient with symbols as keys for each section
112
+ def create_c32_hash(record, doc)
113
+ id_map = build_id_map(doc)
114
+ @section_importers.each_pair do |section, importer|
115
+ record.send(section.to_setter, importer.create_entries(doc, id_map))
116
+ end
117
+ end
118
+
119
+ # Inspects a C32 document and populates the patient Hash with first name, last name
120
+ # birth date and gender.
121
+ #
122
+ # @param [Hash] patient A hash that is used to represent the patient
123
+ # @param [Nokogiri::XML::Node] doc The C32 document parsed by Nokogiri
124
+ def get_demographics(patient, doc)
125
+ patient_element = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:patient')
126
+ patient.first = patient_element.at_xpath('cda:name/cda:given').text
127
+ patient.last = patient_element.at_xpath('cda:name/cda:family').text
128
+ birthdate_in_hl7ts_node = patient_element.at_xpath('cda:birthTime')
129
+ birthdate_in_hl7ts = birthdate_in_hl7ts_node['value']
130
+ patient.birthdate = HL7Helper.timestamp_to_integer(birthdate_in_hl7ts)
131
+ gender_node = patient_element.at_xpath('cda:administrativeGenderCode')
132
+ patient.gender = gender_node['code']
133
+ id_node = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:id')
134
+ patient.medical_record_number = id_node['extension']
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,55 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module C32
4
+
5
+ # TODO Extract Discharge Disposition
6
+ class ProcedureImporter < SectionImporter
7
+
8
+ def initialize
9
+ @entry_xpath = "//cda:procedure[cda:templateId/@root='2.16.840.1.113883.10.20.1.29']"
10
+ @code_xpath = "./cda:code"
11
+ @status_xpath = "./cda:statusCode"
12
+ @description_xpath = "./cda:code/cda:originalText/cda:reference[@value] | ./cda:text/cda:reference[@value] "
13
+ @check_for_usable = true # Pilot tools will set this to false
14
+ end
15
+
16
+ # Traverses that HITSP C32 document passed in using XPath and creates an Array of Entry
17
+ # objects based on what it finds
18
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
19
+ # will have the "cda" namespace registered to "urn:hl7-org:v3"
20
+ # measure definition
21
+ # @return [Array] will be a list of Entry objects
22
+ def create_entries(doc,id_map = {})
23
+ procedure_list = []
24
+ entry_elements = doc.xpath(@entry_xpath)
25
+ entry_elements.each do |entry_element|
26
+ procedure = Procedure.new
27
+ extract_codes(entry_element, procedure)
28
+ extract_dates(entry_element, procedure)
29
+ extract_description(entry_element, procedure, id_map)
30
+ if @check_for_usable
31
+ procedure_list << procedure if procedure.usable?
32
+ else
33
+ procedure_list << procedure
34
+ end
35
+ extract_performer(entry_element, procedure)
36
+ extract_site(entry_element, procedure)
37
+ end
38
+ procedure_list
39
+ end
40
+
41
+ private
42
+
43
+ def extract_performer(parent_element, procedure)
44
+ performer_element = parent_element.at_xpath("./cda:performer")
45
+ procedure.performer = import_actor(performer_element) if performer_element
46
+ end
47
+
48
+ def extract_site(parent_element, procedure)
49
+ procedure.site = extract_code(parent_element, "./cda:targetSiteCode")
50
+ end
51
+
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,58 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module C32
4
+ class ResultImporter < SectionImporter
5
+ def initialize
6
+ @entry_xpath = "//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15.1'] | //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']"
7
+ @code_xpath = "./cda:code"
8
+ @status_xpath = "./cda:statusCode"
9
+ @description_xpath = "./cda:code/cda:originalText/cda:reference[@value] | ./cda:text/cda:reference[@value] "
10
+ @check_for_usable = true # Pilot tools will set this to false
11
+ end
12
+
13
+ # Traverses that HITSP C32 document passed in using XPath and creates an Array of Entry
14
+ # objects based on what it finds
15
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
16
+ # will have the "cda" namespace registered to "urn:hl7-org:v3"
17
+ # measure definition
18
+ # @return [Array] will be a list of Entry objects
19
+ def create_entries(doc,id_map = {})
20
+ result_list = []
21
+ entry_elements = doc.xpath(@entry_xpath)
22
+ entry_elements.each do |entry_element|
23
+ result = LabResult.new
24
+ extract_codes(entry_element, result)
25
+ extract_dates(entry_element, result)
26
+ extract_value(entry_element, result)
27
+ extract_status(entry_element, result)
28
+ extract_description(entry_element, result, id_map)
29
+ extract_interpretation(entry_element, result)
30
+ if @check_for_usable
31
+ result_list << result if result.usable?
32
+ else
33
+ result_list << result
34
+ end
35
+ end
36
+ result_list
37
+ end
38
+
39
+ private
40
+ def extract_interpretation(parent_element, result)
41
+ interpretation_element = parent_element.at_xpath("./cda:interpretationCode")
42
+ if interpretation_element
43
+ code = interpretation_element['code']
44
+ code_system = CodeSystemHelper.code_system_for(interpretation_element['codeSystem'])
45
+ result.interpretation = {'code' => code, 'codeSystem' => code_system}
46
+ end
47
+ end
48
+
49
+ def extract_status(parent_element, result)
50
+ status_code_element = parent_element.at_xpath(@status_xpath)
51
+ if status_code_element
52
+ result.status = status_code_element['code']
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,214 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module C32
4
+ # Class that can be used to create an importer for a section of a HITSP C32 document. It usually
5
+ # operates by selecting all CDA entries in a section and then creates entries for them.
6
+ class SectionImporter
7
+ include HealthDataStandards::Util
8
+
9
+ attr_accessor :check_for_usable
10
+ # Creates a new SectionImporter
11
+ # @param [Hash] id_map A hash of all ID tags to values for the enclosing document. Used to look up descriptions.
12
+ # @param [String] entry_xpath An XPath expression that can be used to find the desired entries
13
+ # @param [String] code_xpath XPath expression to find the code element as a child of the desired CDA entry.
14
+ # Defaults to "./cda:code"
15
+ # @param [String] status_xpath XPath expression to find the status element as a child of the desired CDA
16
+ # entry. Defaults to nil. If not provided, a status will not be checked for since it is not applicable
17
+ # to all enrty types
18
+ def initialize(entry_xpath, code_xpath="./cda:code", status_xpath=nil, description_xpath="./cda:code/cda:originalText/cda:reference[@value] | ./cda:text/cda:reference[@value] ")
19
+ @entry_xpath = entry_xpath
20
+ @code_xpath = code_xpath
21
+ @status_xpath = status_xpath
22
+ @description_xpath = description_xpath
23
+ @check_for_usable = true # Pilot tools will set this to false
24
+ end
25
+
26
+ # @param [String] tag
27
+ # @param [Hash] id_map A map of ids to all tagged text in the narrative portion of a document
28
+ # @return [String] text description of tag
29
+ def lookup_tag(tag, id_map)
30
+ value = id_map[tag]
31
+ # Not sure why, but sometimes the reference is #<Reference> and the ID value is <Reference>, and
32
+ # sometimes it is #<Reference>. We look for both.
33
+ if !value and tag[0] == '#'
34
+ tag = tag[1,tag.length]
35
+ value = id_map[tag]
36
+ end
37
+
38
+ value
39
+ end
40
+
41
+ # Traverses that HITSP C32 document passed in using XPath and creates an Array of Entry
42
+ # objects based on what it finds
43
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
44
+ # will have the "cda" namespace registered to "urn:hl7-org:v3"
45
+ # measure definition
46
+ # @return [Array] will be a list of Entry objects
47
+ def create_entries(doc,id_map = {})
48
+ entry_list = []
49
+ entry_elements = doc.xpath(@entry_xpath)
50
+ entry_elements.each do |entry_element|
51
+ entry = Entry.new
52
+ extract_codes(entry_element, entry)
53
+ extract_dates(entry_element, entry)
54
+ extract_value(entry_element, entry)
55
+ if @status_xpath
56
+ extract_status(entry_element, entry)
57
+ end
58
+ if @description_xpath
59
+ extract_description(entry_element, entry, id_map)
60
+ end
61
+ if @check_for_usable
62
+ entry_list << entry if entry.usable?
63
+ else
64
+ entry_list << entry
65
+ end
66
+ end
67
+ entry_list
68
+ end
69
+
70
+ private
71
+
72
+ def extract_status(parent_element, entry)
73
+ status_element = parent_element.at_xpath(@status_xpath)
74
+ if status_element
75
+ case status_element['code']
76
+ when '55561003'
77
+ entry.status = :active
78
+ when '73425007'
79
+ entry.status = :inactive
80
+ when '413322009'
81
+ entry.status = :resolved
82
+ end
83
+ end
84
+ end
85
+
86
+ def extract_description(parent_element, entry, id_map)
87
+ code_elements = parent_element.xpath(@description_xpath)
88
+ code_elements.each do |code_element|
89
+ tag = code_element['value']
90
+ entry.description = lookup_tag(tag, id_map)
91
+ end
92
+ end
93
+
94
+ def extract_codes(parent_element, entry)
95
+ code_elements = parent_element.xpath(@code_xpath)
96
+ code_elements.each do |code_element|
97
+ add_code_if_present(code_element, entry)
98
+ translations = code_element.xpath('cda:translation')
99
+ translations.each do |translation|
100
+ add_code_if_present(translation, entry)
101
+ end
102
+ end
103
+ end
104
+
105
+ def add_code_if_present(code_element, entry)
106
+ if code_element['codeSystem'] && code_element['code']
107
+ entry.add_code(code_element['code'], CodeSystemHelper.code_system_for(code_element['codeSystem']))
108
+ end
109
+ end
110
+
111
+ def extract_dates(parent_element, entry)
112
+ if parent_element.at_xpath('cda:effectiveTime')
113
+ entry.time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime')['value'])
114
+ end
115
+ if parent_element.at_xpath('cda:effectiveTime/cda:low')
116
+ entry.start_time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime/cda:low')['value'])
117
+ end
118
+ if parent_element.at_xpath('cda:effectiveTime/cda:high')
119
+ entry.end_time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime/cda:high')['value'])
120
+ end
121
+ if parent_element.at_xpath('cda:effectiveTime/cda:center')
122
+ entry.time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime/cda:center')['value'])
123
+ end
124
+ end
125
+
126
+ def extract_value(parent_element, entry)
127
+ value_element = parent_element.at_xpath('cda:value')
128
+ if value_element
129
+ value = value_element['value']
130
+ unit = value_element['unit']
131
+ if value
132
+ entry.set_value(value, unit)
133
+ end
134
+ end
135
+ end
136
+
137
+ def import_actor(actor_element)
138
+ actor_hash = {}
139
+ addresses = actor_element.xpath("./cda:assignedEntity/cda:addr").try(:map) {|ae| import_address(ae)}
140
+ telecoms = actor_element.xpath("./cda:assignedEntity/cda:telecom").try(:map) {|te| import_telecom(te)}
141
+ person_element = actor_element.at_xpath("./cda:assignedEntity/cda:assignedPerson")
142
+ if person_element
143
+ actor_hash['person'] = import_person(person_element)
144
+ actor_hash['person']['addresses'] = addresses
145
+ actor_hash['person']['telecoms'] = telecoms
146
+ end
147
+ organization_element = actor_element.at_xpath("./cda:assignedEntity/cda:assignedOrganization")
148
+ if organization_element
149
+ actor_hash['organization'] = import_organization(organization_element)
150
+ end
151
+
152
+ actor_hash
153
+ end
154
+
155
+ def import_person(person_element)
156
+ person_hash = {}
157
+ name_element = person_element.at_xpath("./cda:name")
158
+ person_hash['name'] = name_element.try(:text)
159
+ person_hash['first'] = name_element.at_xpath("./cda:given").try(:text)
160
+ person_hash['last'] = name_element.at_xpath("./cda:family").try(:text)
161
+ person_hash
162
+ end
163
+
164
+ def import_address(address_element)
165
+ address_hash = {}
166
+ address_hash['streetAddress'] = [address_element.at_xpath("./cda:streetAddressLine").try(:text)]
167
+ address_hash['city'] = address_element.at_xpath("./cda:city").try(:text)
168
+ address_hash['stateOrProvince'] = address_element.at_xpath("./cda:state").try(:text)
169
+ address_hash['zip'] = address_element.at_xpath("./cda:postalCode").try(:text)
170
+ address_hash['country'] = address_element.at_xpath("./cda:country").try(:text)
171
+ address_hash
172
+ end
173
+
174
+ def import_telecom(telecom_element)
175
+ telecom_hash = {}
176
+ telecom_hash['value'] = telecom_element['value']
177
+ telecom_hash['use'] = telecom_element['use']
178
+ telecom_hash
179
+ end
180
+
181
+ def import_organization
182
+ # TODO: Implement when the Patient API has an implementation of
183
+ # organization
184
+ end
185
+
186
+ def extract_code(parent_element, code_xpath, code_system=nil)
187
+ code_element = parent_element.at_xpath(code_xpath)
188
+ code_hash = nil
189
+ if code_element
190
+ code_hash = {'code' => code_element['code']}
191
+ if code_system
192
+ code_hash['codeSystem'] = code_system
193
+ else
194
+ code_hash['codeSystemOid'] = code_element['codeSystem']
195
+ code_hash['codeSystem'] = CodeSystemHelper.code_system_for(code_hash['codeSystemOid'])
196
+ end
197
+ end
198
+
199
+ code_hash
200
+ end
201
+
202
+ def extract_scalar(parent_element, scalar_xpath)
203
+ scalar_element = parent_element.at_xpath(scalar_xpath)
204
+ if scalar_element
205
+ {'unit' => scalar_element['unit'], 'value' => scalar_element['value'].to_i}
206
+ else
207
+ nil
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+