health-data-standards 0.7.0 → 0.7.1

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