health-data-standards 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
+