health-data-standards 0.7.1 → 0.8.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.
- data/Gemfile +9 -1
- data/Rakefile +14 -4
- data/lib/health-data-standards.rb +23 -0
- data/lib/health-data-standards/export/ccr.rb +55 -56
- data/lib/health-data-standards/export/hdata/metadata.rb +16 -0
- data/lib/health-data-standards/export/html.rb +21 -0
- data/lib/health-data-standards/export/template_helper.rb +3 -0
- data/lib/health-data-standards/export/view_helper.rb +29 -5
- data/lib/health-data-standards/import/c32/condition_importer.rb +31 -33
- data/lib/health-data-standards/import/c32/encounter_importer.rb +4 -6
- data/lib/health-data-standards/import/c32/medication_importer.rb +1 -5
- data/lib/health-data-standards/import/c32/organization_importer.rb +23 -0
- data/lib/health-data-standards/import/c32/patient_importer.rb +1 -4
- data/lib/health-data-standards/import/c32/provider_importer.rb +43 -30
- data/lib/health-data-standards/import/c32/section_importer.rb +20 -41
- data/lib/health-data-standards/import/ccr/patient_importer.rb +27 -10
- data/lib/health-data-standards/import/ccr/provider_importer.rb +29 -41
- data/lib/health-data-standards/import/ccr/section_importer.rb +38 -27
- data/lib/health-data-standards/import/green_c32/allergy_importer.rb +20 -0
- data/lib/health-data-standards/import/green_c32/condition_importer.rb +2 -3
- data/lib/health-data-standards/import/green_c32/encounter_importer.rb +42 -0
- data/lib/health-data-standards/import/green_c32/immunization_importer.rb +23 -0
- data/lib/health-data-standards/import/green_c32/medication_importer.rb +69 -0
- data/lib/health-data-standards/import/green_c32/procedure_importer.rb +35 -0
- data/lib/health-data-standards/import/green_c32/result_importer.rb +21 -8
- data/lib/health-data-standards/import/green_c32/section_importer.rb +55 -9
- data/lib/health-data-standards/import/green_c32/social_history_importer.rb +18 -0
- data/lib/health-data-standards/import/green_c32/vital_sign_importer.rb +21 -0
- data/lib/health-data-standards/import/hdata/metadata_importer.rb +82 -0
- data/lib/health-data-standards/import/provider_import_utils.rb +23 -0
- data/lib/health-data-standards/models/address.rb +11 -0
- data/lib/health-data-standards/models/allergy.rb +1 -0
- data/lib/health-data-standards/models/condition.rb +1 -1
- data/lib/health-data-standards/models/encounter.rb +11 -6
- data/lib/health-data-standards/models/entry.rb +16 -5
- data/lib/health-data-standards/models/fulfillment_history.rb +3 -5
- data/lib/health-data-standards/models/immunization.rb +7 -1
- data/lib/health-data-standards/models/medication.rb +4 -3
- data/lib/health-data-standards/models/metadata/author.rb +16 -0
- data/lib/health-data-standards/models/metadata/base.rb +20 -0
- data/lib/health-data-standards/models/metadata/change_info.rb +9 -0
- data/lib/health-data-standards/models/metadata/link_info.rb +9 -0
- data/lib/health-data-standards/models/metadata/pedigree.rb +15 -0
- data/lib/health-data-standards/models/organization.rb +8 -0
- data/lib/health-data-standards/models/procedure.rb +5 -2
- data/lib/health-data-standards/models/provider.rb +6 -1
- data/lib/health-data-standards/models/record.rb +13 -3
- data/lib/health-data-standards/models/social_history.rb +3 -0
- data/lib/health-data-standards/models/telecom.rb +9 -0
- data/lib/health-data-standards/models/vital_sign.rb +2 -0
- data/lib/health-data-standards/util/code_system_helper.rb +3 -1
- data/templates/_address.gc32.erb +9 -0
- data/templates/_allergies.c32.erb +2 -2
- data/templates/_allergy.gc32.erb +13 -0
- data/templates/_care_goals.c32.erb +1 -1
- data/templates/_condition.gc32.erb +6 -6
- data/templates/_conditions.c32.erb +2 -2
- data/templates/_encounter.gc32.erb +32 -0
- data/templates/_encounters.c32.erb +1 -1
- data/templates/_immunization.gc32.erb +9 -0
- data/templates/_immunizations.c32.erb +1 -1
- data/templates/_medical_equipment.c32.erb +1 -1
- data/templates/_medication.gc32.erb +60 -0
- data/templates/_medications.c32.erb +1 -1
- data/templates/_narrative_block.c32.erb +1 -1
- data/templates/_organization.gc32.erb +10 -0
- data/templates/_pedigree.hdata.erb +24 -0
- data/templates/_procedure.gc32.erb +8 -0
- data/templates/_procedures.c32.erb +1 -1
- data/templates/_provider.gc32.erb +19 -0
- data/templates/_results.c32.erb +1 -1
- data/templates/_social_history.c32.erb +1 -1
- data/templates/_social_history.gc32.erb +6 -0
- data/templates/_telecom.gc32.erb +1 -0
- data/templates/_vital_sign.gc32.erb +12 -0
- data/templates/_vital_signs.c32.erb +1 -1
- data/templates/metadata.hdata.erb +35 -0
- data/templates/show.html.erb +287 -0
- metadata +50 -15
@@ -49,13 +49,11 @@ module HealthDataStandards
|
|
49
49
|
|
50
50
|
def extract_facility(parent_element, encounter)
|
51
51
|
participant_element = parent_element.at_xpath("./cda:participant[@typeCode='LOC']/cda:participantRole[@classCode='SDLOC']")
|
52
|
-
encounter.facility = {}
|
53
52
|
if (participant_element)
|
54
|
-
|
55
|
-
addresses = participant_element.xpath("./cda:addr").try(:map) {|ae| import_address(ae)}
|
56
|
-
|
57
|
-
|
58
|
-
encounter.facility['telcoms'] = telecoms
|
53
|
+
org = Organization.new(name: participant_element.at_xpath("./cda:playingEntity/cda:name").try(:text))
|
54
|
+
org.addresses = participant_element.xpath("./cda:addr").try(:map) {|ae| import_address(ae)}
|
55
|
+
org.telecoms = participant_element.xpath("./cda:telecom").try(:map) {|te| import_telecom(te)}
|
56
|
+
encounter.facility = org
|
59
57
|
end
|
60
58
|
end
|
61
59
|
|
@@ -63,7 +63,7 @@ module HealthDataStandards
|
|
63
63
|
if @check_for_usable
|
64
64
|
medication_list << medication if medication.usable?
|
65
65
|
else
|
66
|
-
medication_list <<
|
66
|
+
medication_list << medication
|
67
67
|
end
|
68
68
|
end
|
69
69
|
medication_list
|
@@ -80,10 +80,6 @@ module HealthDataStandards
|
|
80
80
|
actor_element = fh_element.at_xpath('./cda:performer')
|
81
81
|
if actor_element
|
82
82
|
fulfillment_history.provider = import_actor(actor_element)
|
83
|
-
addr_element = actor_element.at_xpath("./cda:assignedEntity/cda:addr")
|
84
|
-
if addr_element
|
85
|
-
fulfillment_history.dispensing_pharmacy_location = import_address(addr_element)
|
86
|
-
end
|
87
83
|
end
|
88
84
|
hl7_timestamp = fh_element.at_xpath('./cda:effectiveTime').try(:[], 'value')
|
89
85
|
fulfillment_history.dispense_date = HL7Helper.timestamp_to_integer(hl7_timestamp) if hl7_timestamp
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HealthDataStandards
|
2
|
+
module Import
|
3
|
+
module C32
|
4
|
+
class OrganizationImporter < SectionImporter
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
def extract_organization(org_element)
|
12
|
+
return unless org_element
|
13
|
+
org = Organization.new
|
14
|
+
org.name = org_element.at_xpath("./name").try(:text)
|
15
|
+
org.addresses = org_element.xpath("./cda:addr").map { |addr| import_address(addr) }
|
16
|
+
org.telecoms = org_element.xpath("./cda:telecom").map { |tele| import_telecom(tele) }
|
17
|
+
org
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -59,10 +59,7 @@ module HealthDataStandards
|
|
59
59
|
@section_importers[:results] = ResultImporter.new
|
60
60
|
@section_importers[:vital_signs] = VitalSignImporter.new
|
61
61
|
@section_importers[:medications] = MedicationImporter.new
|
62
|
-
@section_importers[:conditions] =
|
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]")
|
62
|
+
@section_importers[:conditions] = ConditionImporter.new
|
66
63
|
@section_importers[:social_history] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']")
|
67
64
|
@section_importers[:care_goals] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']")
|
68
65
|
@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",
|
@@ -1,47 +1,62 @@
|
|
1
1
|
require "date"
|
2
|
-
require "date/delta"
|
2
|
+
# require "date/delta"
|
3
3
|
|
4
4
|
module HealthDataStandards
|
5
5
|
module Import
|
6
6
|
module C32
|
7
|
-
class ProviderImporter
|
7
|
+
class ProviderImporter < SectionImporter
|
8
|
+
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
|
12
|
+
end
|
13
|
+
|
8
14
|
include Singleton
|
9
|
-
|
15
|
+
include ProviderImportUtils
|
10
16
|
# Extract Healthcare Providers from C32
|
11
17
|
#
|
12
18
|
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
13
19
|
# will have the "cda" namespace registered to "urn:hl7-org:v3"
|
14
20
|
# @return [Array] an array of providers found in the document
|
15
|
-
def extract_providers(doc
|
16
|
-
|
17
|
-
|
18
|
-
|
21
|
+
def extract_providers(doc)
|
22
|
+
performers = doc.xpath("//cda:documentationOf/cda:serviceEvent/cda:performer")
|
23
|
+
performers.map do |performer|
|
24
|
+
provider_perf = extract_provider_data(performer, true)
|
25
|
+
ProviderPerformance.new(start_date: provider_perf.delete(:start), end_date: provider_perf.delete(:end), provider: find_or_create_provider(provider_perf))
|
26
|
+
end
|
27
|
+
end
|
19
28
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
private
|
30
|
+
|
31
|
+
def extract_provider_data(performer, use_dates=true)
|
32
|
+
provider = {}
|
33
|
+
entity = performer.xpath("./cda:assignedEntity")
|
34
|
+
name = entity.xpath("./cda:assignedPerson/cda:name")
|
35
|
+
provider[:title] = extract_data(name, "./cda:prefix")
|
36
|
+
provider[:given_name] = extract_data(name, "./cda:given[1]")
|
37
|
+
provider[:family_name] = extract_data(name, "./cda:family")
|
38
|
+
provider[:organization] = OrganizationImporter.instance.extract_organization(entity.at_xpath("./cda:representedOrganization"))
|
39
|
+
provider[:specialty] = extract_data(entity, "./cda:code/@code")
|
40
|
+
time = performer.xpath(performer, "./cda:time")
|
41
|
+
|
42
|
+
if use_dates
|
31
43
|
provider[:start] = extract_date(time, "./cda:low/@value")
|
32
44
|
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
45
|
end
|
46
|
+
|
47
|
+
# NIST sample C32s use different OID for NPI vs C83, support both
|
48
|
+
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")
|
49
|
+
provider[:addresses] = performer.xpath("./cda:assignedEntity/cda:addr").try(:map) {|ae| import_address(ae)}
|
50
|
+
provider[:telecoms] = performer.xpath("./cda:assignedEntity/cda:telecom").try(:map) {|te| import_telecom(te)}
|
51
|
+
|
52
|
+
provider[:npi] = npi if Provider.valid_npi?(npi)
|
53
|
+
provider
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_or_create_provider(provider_hash)
|
57
|
+
provider = Provider.first(conditions: {npi: provider_hash[:npi]}) if provider_hash[:npi]
|
58
|
+
provider ||= Provider.create(provider_hash)
|
42
59
|
end
|
43
|
-
|
44
|
-
private
|
45
60
|
|
46
61
|
def extract_date(subject,query)
|
47
62
|
date = extract_data(subject,query)
|
@@ -53,8 +68,6 @@ module HealthDataStandards
|
|
53
68
|
result = subject.xpath(query).text
|
54
69
|
if result == ""
|
55
70
|
nil
|
56
|
-
elsif block_given?
|
57
|
-
yield(result)
|
58
71
|
else
|
59
72
|
result
|
60
73
|
end
|
@@ -135,54 +135,33 @@ module HealthDataStandards
|
|
135
135
|
end
|
136
136
|
|
137
137
|
def import_actor(actor_element)
|
138
|
-
|
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
|
141
|
-
addresses = actor_element.xpath("./cda:assignedEntity/cda:addr").try(:map) {|ae| import_address(ae)}
|
142
|
-
telecoms = actor_element.xpath("./cda:assignedEntity/cda:telecom").try(:map) {|te| import_telecom(te)}
|
143
|
-
person_element = actor_element.at_xpath("./cda:assignedEntity/cda:assignedPerson")
|
144
|
-
if person_element
|
145
|
-
actor_hash['person'] = import_person(person_element)
|
146
|
-
actor_hash['person']['addresses'] = addresses
|
147
|
-
actor_hash['person']['telecoms'] = telecoms
|
148
|
-
end
|
149
|
-
organization_element = actor_element.at_xpath("./cda:assignedEntity/cda:assignedOrganization")
|
150
|
-
if organization_element
|
151
|
-
actor_hash['organization'] = import_organization(organization_element)
|
152
|
-
end
|
153
|
-
|
154
|
-
actor_hash
|
138
|
+
return ProviderImporter.instance.extract_provider(actor_element)
|
155
139
|
end
|
156
140
|
|
157
|
-
def import_person(person_element)
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
end
|
141
|
+
# def import_person(person_element)
|
142
|
+
# person_hash = {}
|
143
|
+
# name_element = person_element.at_xpath("./cda:name")
|
144
|
+
# person_hash['name'] = name_element.try(:text)
|
145
|
+
# person_hash['first'] = name_element.at_xpath("./cda:given").try(:text)
|
146
|
+
# person_hash['last'] = name_element.at_xpath("./cda:family").try(:text)
|
147
|
+
# person_hash
|
148
|
+
# end
|
165
149
|
|
166
150
|
def import_address(address_element)
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
151
|
+
address = Address.new
|
152
|
+
address.street = [address_element.at_xpath("./cda:streetAddressLine").try(:text)]
|
153
|
+
address.city = address_element.at_xpath("./cda:city").try(:text)
|
154
|
+
address.state = address_element.at_xpath("./cda:state").try(:text)
|
155
|
+
address.zip = address_element.at_xpath("./cda:postalCode").try(:text)
|
156
|
+
address.country = address_element.at_xpath("./cda:country").try(:text)
|
157
|
+
address
|
174
158
|
end
|
175
159
|
|
176
160
|
def import_telecom(telecom_element)
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
end
|
182
|
-
|
183
|
-
def import_organization
|
184
|
-
# TODO: Implement when the Patient API has an implementation of
|
185
|
-
# organization
|
161
|
+
tele = Telecom.new
|
162
|
+
tele.value = telecom_element['value']
|
163
|
+
tele.use = telecom_element['use']
|
164
|
+
tele
|
186
165
|
end
|
187
166
|
|
188
167
|
def extract_code(parent_element, code_xpath, code_system=nil)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "time"
|
2
|
+
|
1
3
|
module HealthDataStandards
|
2
4
|
module Import
|
3
5
|
module CCR
|
@@ -61,7 +63,7 @@ module HealthDataStandards
|
|
61
63
|
@section_importers[:conditions] = SimpleImporter.new("//ccr:Problems/ccr:Problem",:conditions)
|
62
64
|
@section_importers[:social_history] = SimpleImporter.new("//ccr:SocialHistory/ccr:SocialHistoryElement", :social_history)
|
63
65
|
@section_importers[:care_goals] = SimpleImporter.new("//ccr:Goals/ccr:Goal",:care_goals)
|
64
|
-
@section_importers[:medical_equipment] = ProductImporter.new("//ccr:
|
66
|
+
@section_importers[:medical_equipment] = ProductImporter.new("//ccr:Equipment/ccr:EquipmentElement",:medical_equipment)
|
65
67
|
@section_importers[:allergies] = SimpleImporter.new("//ccr:Alerts/ccr:Alert",:allergies)
|
66
68
|
@section_importers[:immunizations] = ProductImporter.new("//ccr:Immunizations/ccr:Immunization",:immunizations)
|
67
69
|
end
|
@@ -79,10 +81,10 @@ module HealthDataStandards
|
|
79
81
|
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
80
82
|
# will have the "ccr" namespace registered to ""urn:astm-org:CCR""
|
81
83
|
# @return [Hash] a representation of the patient that can be inserted into MongoDB
|
82
|
-
def parse_ccr(doc)
|
84
|
+
def parse_ccr(doc, patient_id_xpath="//ccr:ContinuityOfCareRecord/ccr:Patient/ccr:ActorID")
|
83
85
|
ccr_patient = {}
|
84
86
|
entries = create_hash(doc)
|
85
|
-
get_demographics(ccr_patient, doc)
|
87
|
+
get_demographics(ccr_patient, doc, patient_id_xpath)
|
86
88
|
process_events(ccr_patient, entries)
|
87
89
|
Record.new(ccr_patient)
|
88
90
|
end
|
@@ -181,21 +183,36 @@ module HealthDataStandards
|
|
181
183
|
#
|
182
184
|
# @param [Hash] patient A hash that is used to represent the patient
|
183
185
|
# @param [Nokogiri::XML::Node] doc The CCR document parsed by Nokogiri
|
184
|
-
def get_demographics(patient, doc)
|
185
|
-
|
186
|
-
patientActor = doc.at_xpath("//ccr:ContinuityOfCareRecord/ccr:Actors/ccr:Actor[ccr:ActorObjectID = \"#{
|
186
|
+
def get_demographics(patient, doc, patient_id_xpath)
|
187
|
+
patientActorID = doc.at_xpath("//ccr:ContinuityOfCareRecord/ccr:Patient/ccr:ActorID").content
|
188
|
+
patientActor = doc.at_xpath("//ccr:ContinuityOfCareRecord/ccr:Actors/ccr:Actor[ccr:ActorObjectID = \"#{patientActorID}\"]")
|
189
|
+
patientID = patientActor.at_xpath(patient_id_xpath).try(:content)
|
190
|
+
patientID ||= patientActorID
|
191
|
+
|
187
192
|
patient['first'] = patientActor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Given').content
|
188
193
|
patient['last'] = patientActor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Family').content
|
189
194
|
birthdate = patientActor.at_xpath('./ccr:Person//ccr:DateOfBirth/ccr:ExactDateTime | ./ccr:Person//ccr:DateOfBirth/ccr:ApproximateDateTime')
|
190
|
-
patient['birthdate'] = Time.
|
195
|
+
patient['birthdate'] = Time.parse(birthdate.content).to_i if birthdate
|
191
196
|
|
192
197
|
gender_string = patientActor.at_xpath('./ccr:Person/ccr:Gender/ccr:Text').content.downcase
|
193
198
|
patient['gender'] = Gender[gender_string.downcase]
|
194
|
-
|
195
199
|
#race_node = doc.at_xpath('/ccr:placeholder') #how do you find this?
|
196
|
-
|
200
|
+
race = doc.at_xpath('//ccr:SocialHistory/ccr:SocialHistoryElement[./ccr:Type/ccr:Text = "Race"]/ccr:Description/ccr:Code[./ccr:CodingSystem = "CDC-RE"]/ccr:Value')
|
201
|
+
ethnicity = doc.at_xpath('//ccr:SocialHistory/ccr:SocialHistoryElement[./ccr:Type/ccr:Text = "Ethnicity"]/ccr:Description/ccr:Code[./ccr:CodingSystem = "CDC-RE"]/ccr:Value')
|
202
|
+
|
203
|
+
if ethnicity
|
204
|
+
patient[:ethnicity] = {"code" => ethnicity.text, "codeSystem" => 'CDC-RE'}
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
if race
|
209
|
+
patient[:race] = {"code" => race.text, "codeSystem" => 'CDC-RE'}
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
|
197
214
|
#ethnicity_node = doc.at_xpath()
|
198
|
-
|
215
|
+
|
199
216
|
|
200
217
|
# languages = doc.at_xpath()
|
201
218
|
patient['languages'] = nil
|
@@ -1,60 +1,48 @@
|
|
1
1
|
require "date"
|
2
|
-
require "date/delta"
|
2
|
+
# require "date/delta"
|
3
3
|
|
4
4
|
module HealthDataStandards
|
5
5
|
module Import
|
6
6
|
module CCR
|
7
7
|
class ProviderImporter
|
8
8
|
include Singleton
|
9
|
+
include ProviderImportUtils
|
9
10
|
|
10
11
|
# Extract Healthcare Providers from CCR
|
11
12
|
#
|
12
13
|
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
13
14
|
# will have the "ccr" namespace registered to "urn:astm-org:CCR"
|
14
15
|
# @return [Array] an array of providers found in the document
|
15
|
-
|
16
|
+
|
17
|
+
def create_provider(actor)
|
18
|
+
# Differentiate care providers by content of this field
|
19
|
+
provider = {}
|
20
|
+
if actor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Given')
|
21
|
+
provider[:given_name] = extract_data(actor, './ccr:Person/ccr:Name/ccr:CurrentName/ccr:Given')
|
22
|
+
provider[:family_name] = extract_data(actor, './ccr:Person/ccr:Name/ccr:CurrentName/ccr:Family')
|
23
|
+
provider[:specialty] = extract_data(actor, './ccr:Specialty/ccr:Text')
|
24
|
+
end
|
25
|
+
|
26
|
+
provider[:specialty] = extract_data(actor, './ccr:Specialty/ccr:Text')
|
16
27
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
28
|
+
|
29
|
+
npi_ids = actor.at_xpath("./ccr:IDs[ccr:Type/ccr:Text = \"NPI\"]")
|
30
|
+
if npi_ids
|
31
|
+
npi_id = npi_ids.at_xpath("./ccr:ID")
|
32
|
+
npi = npi_id.content
|
33
|
+
provider[:npi] = npi if Provider.valid_npi?(npi)
|
22
34
|
end
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
actor.at_xpath("./ccr:Source/ccr:Actor/ccr:ActorRole/ccr:Text").content.downcase =~ /care provider/
|
31
|
-
provider = {}
|
32
|
-
if actor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Given')
|
33
|
-
provider[:given_name] = actor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Given').content
|
34
|
-
provider[:family_name] = actor.at_xpath('./ccr:Person/ccr:Name/ccr:CurrentName/ccr:Family').content
|
35
|
-
end
|
36
|
-
if actor.at_xpath('./ccr:Specialty/ccr:Text')
|
37
|
-
provider[:specialty] = actor.at_xpath('./ccr:Specialty/ccr:Text').content
|
38
|
-
end
|
39
|
-
if actor.at_xpath("ccr:Telephone/ccr:Value")
|
40
|
-
provider[:phone] = actor.at_xpath("ccr:Telephone/ccr:Value").content
|
41
|
-
end
|
42
|
-
# Not clear precisely how NPI would be specified
|
43
|
-
npi_ids = actor.at_xpath("./ccr:IDs[ccr:Type/ccr:Text = \"NPI\"]")
|
44
|
-
if npi_ids
|
45
|
-
npi_id = npi_ids.at_xpath("./ccr:ID")
|
46
|
-
npi = npi_id.content
|
47
|
-
if Provider.valid_npi?(npi)
|
48
|
-
provider[:npi] = npi
|
49
|
-
else
|
50
|
-
puts "Warning: Invalid NPI (#{npi})"
|
51
|
-
end #valid NPI
|
52
|
-
end #has NPI
|
35
|
+
|
36
|
+
# binding.pry
|
37
|
+
find_or_create_provider(provider)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def extract_providers(doc)
|
53
42
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end.compact
|
43
|
+
# Providers are identified as the 'Source' for entries in the CCR. Sources can also include the patient, relatives, insurance companies, etc
|
44
|
+
provider_elements = doc.xpath("//ccr:ContinuityOfCareRecord/ccr:Actors/ccr:Actor[ccr:IDs/ccr:Type/ccr:Text=\"NPI\"]")
|
45
|
+
provider_elements.map { |pv| ProviderPerformance.new(provider: create_provider(pv)) }
|
58
46
|
end
|
59
47
|
end
|
60
48
|
end
|
@@ -4,6 +4,29 @@ module HealthDataStandards
|
|
4
4
|
# Class that can be used to create an importer for a section of a ASTM CCR document. It usually
|
5
5
|
# operates by selecting all CCR entries in a section and then creates entries for them.
|
6
6
|
class SectionImporter
|
7
|
+
|
8
|
+
CODE_SYSTEM_MAP = {
|
9
|
+
"lnc" => "LOINC",
|
10
|
+
"loinc" => "LOINC",
|
11
|
+
"cpt" => "CPT",
|
12
|
+
"cpt-4" => "CPT",
|
13
|
+
"sct" => "SNOMED-CT",
|
14
|
+
"snomedct" => "SNOMED-CT",
|
15
|
+
"snomed-ct" => "SNOMED-CT",
|
16
|
+
"rxnorm" => "RxNorm",
|
17
|
+
"i9cdx" => "ICD-9-CM",
|
18
|
+
"icd-9-cm" => "ICD-9-CM",
|
19
|
+
"icd9-cm" => "ICD-9-CM",
|
20
|
+
"icd9" => "ICD-9-CM",
|
21
|
+
"icd10-cm" => "ICD-9-CM",
|
22
|
+
"icd10" => "ICD-9-CM",
|
23
|
+
"cvx" => "CVX",
|
24
|
+
"hcpcs" => "HCPCS",
|
25
|
+
"cdc" => "CDC-RE",
|
26
|
+
"CDC" => "CDC-RE",
|
27
|
+
"cdc-re" => "CDC-RE"
|
28
|
+
|
29
|
+
}
|
7
30
|
attr_accessor :check_for_usable
|
8
31
|
# Creates a new SectionImporter
|
9
32
|
# @param [String] entry_xpath An XPath expression that can be used to find the desired entries
|
@@ -19,37 +42,23 @@ module HealthDataStandards
|
|
19
42
|
# in the tree, and the side effect is to edit the CodingSystem subnode.
|
20
43
|
# @param [String] code - Input is a single "Code" node
|
21
44
|
def normalize_coding_system(code)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
"cpt-4" => "CPT",
|
27
|
-
"snomedct" => "SNOMED-CT",
|
28
|
-
"snomed-ct" => "SNOMED-CT",
|
29
|
-
"rxnorm" => "RxNorm",
|
30
|
-
"icd9-cm" => "ICD-9-CM",
|
31
|
-
"icd9" => "ICD-9-CM",
|
32
|
-
"icd10-cm" => "ICD-9-CM",
|
33
|
-
"icd10" => "ICD-9-CM",
|
34
|
-
"cvx" => "CVX",
|
35
|
-
"hcpcs" => "HCPCS"
|
36
|
-
|
37
|
-
}
|
38
|
-
codingsystem = lookup[code.xpath('./ccr:CodingSystem')[0].content.downcase]
|
39
|
-
if(codingsystem)
|
40
|
-
code.xpath('./ccr:CodingSystem')[0].content = codingsystem
|
45
|
+
coding_system = code.xpath('./ccr:CodingSystem')[0].content.downcase
|
46
|
+
coding_system_value = CODE_SYSTEM_MAP[coding_system]
|
47
|
+
if(coding_system_value)
|
48
|
+
code.xpath('./ccr:CodingSystem')[0].content = coding_system_value
|
41
49
|
end
|
42
50
|
end
|
43
51
|
|
44
52
|
def extract_status(parent_element, entry)
|
45
53
|
status_element = parent_element.at_xpath('./ccr:Status')
|
46
54
|
if status_element
|
47
|
-
|
48
|
-
|
55
|
+
status_text = parent_element.at_xpath('./ccr:Status/ccr:Text')
|
56
|
+
return unless status_text
|
57
|
+
status = status_text.content.downcase
|
49
58
|
if %w(active inactive resolved).include?(status)
|
50
59
|
entry.status = status.to_sym
|
51
60
|
end
|
52
|
-
end
|
61
|
+
end
|
53
62
|
end
|
54
63
|
|
55
64
|
|
@@ -72,7 +81,7 @@ module HealthDataStandards
|
|
72
81
|
# Time is supposed to be in iso8601, but seems like we need to handle simple YYYY-MM-DD as well
|
73
82
|
def extract_time(datetime)
|
74
83
|
return unless datetime
|
75
|
-
Time.parse(datetime).to_i
|
84
|
+
Time.parse(datetime).to_i rescue nil
|
76
85
|
end
|
77
86
|
|
78
87
|
def extract_dates(parent_element, entry)
|
@@ -95,10 +104,12 @@ module HealthDataStandards
|
|
95
104
|
end
|
96
105
|
|
97
106
|
def extract_value(parent_element, entry)
|
98
|
-
|
99
|
-
if
|
100
|
-
|
101
|
-
|
107
|
+
result_element = parent_element.at_xpath('./ccr:TestResult')
|
108
|
+
if result_element
|
109
|
+
value_element = result_element.at_xpath('./ccr:Value')
|
110
|
+
value = value_element ? value_element.content : nil
|
111
|
+
unit_element = result_element.at_xpath('./ccr:Units/ccr:Unit')
|
112
|
+
unit = unit_element ? unit_element.content : nil
|
102
113
|
if value
|
103
114
|
entry.set_value(value, unit)
|
104
115
|
end
|