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
@@ -0,0 +1,60 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module CCR
4
+ class ProductImporter < SectionImporter
5
+
6
+ # Traverses that ASTM CCR document passed in using XPath and creates an Array of Entry
7
+ # objects based on what it finds
8
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
9
+ # will have the "ccr" namespace registered to "urn:astm-org:CCR"
10
+ # measure definition
11
+ # @return [Array] will be a list of Entry objects
12
+ def create_entries(doc)
13
+ entry_list = []
14
+ entry_elements = doc.xpath(@entry_xpath)
15
+ entry_elements.each do |entry_element|
16
+ entry = Entry.new
17
+ product = entry_element.at_xpath("./ccr:Product")
18
+ process_product(product,entry)
19
+ extract_dates(entry_element, entry)
20
+ extract_status(entry_element, entry)
21
+ if @check_for_usable
22
+ entry_list << entry if entry.usable?
23
+ else
24
+ entry_list << entry
25
+ end
26
+ end
27
+ entry_list
28
+ end
29
+
30
+ # Add the codes from a <Product> block subsection to an Entry
31
+ def process_product_codes(node, entry)
32
+ codes = node.xpath("./ccr:Code")
33
+ if codes.size > 0
34
+ found_code = true
35
+ codes.each do |code|
36
+ normalize_coding_system(code)
37
+ codetext = code.at_xpath("./ccr:CodingSystem").content
38
+ entry.add_code(code.at_xpath("./ccr:Value").content, codetext)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Special handling for the medications section
44
+ def process_product (product, entry)
45
+ productName = product.at_xpath("./ccr:ProductName")
46
+ brandName = product.at_xpath("./ccr:BrandName")
47
+ productNameText = productName.at_xpath("./ccr:Text")
48
+ brandNameText = brandName.at_xpath("./ccr:Text") if brandName
49
+ entry.description = productNameText.content
50
+ process_product_codes(productName, entry) # we throw any codes found within the productName and brandName into the same entry
51
+ process_product_codes(brandName, entry) if brandName
52
+ end
53
+
54
+
55
+ def create_product_entries(doc)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,62 @@
1
+ require "date"
2
+ require "date/delta"
3
+
4
+ module HealthDataStandards
5
+ module Import
6
+ module CCR
7
+ class ProviderImporter
8
+ include Singleton
9
+
10
+ # Extract Healthcare Providers from CCR
11
+ #
12
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
13
+ # will have the "ccr" namespace registered to "urn:astm-org:CCR"
14
+ # @return [Array] an array of providers found in the document
15
+ def extract_providers(doc)
16
+
17
+ # Providers are identified as the 'Source' for entries in the CCR. Sources can also include the patient, relatives, insurance companies, etc
18
+ actorIDs = doc.xpath("//ccr:Source/ccr:Actor/ccr:ActorID")
19
+ uniqueActorIDs = {}
20
+ actorIDs.each do |actorID|
21
+ uniqueActorIDs[actorID.content] = actorID.content
22
+ end
23
+ actorIDs = uniqueActorIDs.keys
24
+ providers = actorIDs.map do |actorID|
25
+ provider = nil
26
+ actor = doc.at_xpath("//ccr:ContinuityOfCareRecord/ccr:Actors/ccr:Actor[ccr:ActorObjectID = \"#{actorID}\"]")
27
+ if(actor)
28
+ # Differentiate care providers by content of this field
29
+ if actor.at_xpath("./ccr:Source/ccr:Actor/ccr:ActorRole/ccr:Text") &&
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
53
+
54
+ end #is a provider
55
+ end #is an actor
56
+ provider
57
+ end.compact
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module CCR
4
+ class ResultImporter < SectionImporter
5
+
6
+ # Traverses that ASTM CCR document passed in using XPath and creates an Array of Entry
7
+ # objects based on what it finds
8
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
9
+ # will have the "ccr" namespace registered to "urn:astm-org:CCR"
10
+ # measure definition
11
+ # @return [Array] will be a list of Entry objects
12
+ def create_entries(doc)
13
+ entry_list = []
14
+ entry_elements = doc.xpath(@entry_xpath)
15
+ entry_elements.each do |entry_element|
16
+ # Grab the time and the description from the Result node
17
+ dummy_entry = Entry.new
18
+ extract_dates(entry_element, dummy_entry)
19
+ dummy_entry.description = ""
20
+ if entry_element.at_xpath("./ccr:Description/ccr:Text")
21
+ dummy_entry.description = entry_element.at_xpath("./ccr:Description/ccr:Text").content
22
+ end
23
+ # Iterate over embedded tests
24
+ # Grab the values and the description from the Test nodes
25
+ # For each test, create an entry with the time from the Result, the description a concatenation of the Result and Test descriptions,
26
+ # and the value from the Test
27
+
28
+ tests = entry_element.xpath("./ccr:Test")
29
+ tests.each do |test|
30
+ entry = Entry.new
31
+ entry = dummy_entry.clone # copies time and description
32
+ extract_codes(test, entry)
33
+ extract_value(test, entry)
34
+ extract_status(test, entry)
35
+ extract_dates(test, entry)
36
+ entry.description = dummy_entry.description + ": " + entry.description
37
+ if @check_for_usable
38
+ entry_list << entry if entry.usable?
39
+ else
40
+ entry_list << entry
41
+ end
42
+ end
43
+ end
44
+ entry_list
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,124 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module CCR
4
+ # Class that can be used to create an importer for a section of a ASTM CCR document. It usually
5
+ # operates by selecting all CCR entries in a section and then creates entries for them.
6
+ class SectionImporter
7
+ attr_accessor :check_for_usable
8
+ # Creates a new SectionImporter
9
+ # @param [String] entry_xpath An XPath expression that can be used to find the desired entries
10
+ # @param [String] section_name name of the section. There is some section-dependent processing
11
+ def initialize(entry_xpath, section_name)
12
+ @entry_xpath = entry_xpath
13
+ @section_name = section_name
14
+ @check_for_usable = true # Pilot tools will set this to false
15
+ end
16
+
17
+ # normalize_coding_system attempts to simplify analysis of the XML doc by
18
+ # normalizing the names of the coding systems. Input is a single "Code" node
19
+ # in the tree, and the side effect is to edit the CodingSystem subnode.
20
+ # @param [String] code - Input is a single "Code" node
21
+ def normalize_coding_system(code)
22
+ lookup = {
23
+ "lnc" => "LOINC",
24
+ "loinc" => "LOINC",
25
+ "cpt" => "CPT",
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
41
+ end
42
+ end
43
+
44
+ def extract_status(parent_element, entry)
45
+ status_element = parent_element.at_xpath('./ccr:Status')
46
+ if status_element
47
+ status = parent_element.at_xpath('./ccr:Status/ccr:Text').content.downcase
48
+
49
+ if %w(active inactive resolved).include?(status)
50
+ entry.status = status.to_sym
51
+ end
52
+ end
53
+ end
54
+
55
+
56
+ # Add the codes from a <Code> block to an Entry
57
+ def extract_codes(parent_element, entry)
58
+ codes = parent_element.xpath("./ccr:Description/ccr:Code")
59
+ entry.description = ""
60
+ if (parent_element.at_xpath("./ccr:Description/ccr:Text") )
61
+ entry.description = parent_element.at_xpath("./ccr:Description/ccr:Text").content
62
+ end
63
+ if codes.size > 0
64
+ found_code = true
65
+ codes.each do |code|
66
+ normalize_coding_system(code)
67
+ entry.add_code(code.at_xpath("./ccr:Value").content, code.at_xpath("./ccr:CodingSystem").content)
68
+ end
69
+ end
70
+ end
71
+
72
+ # Time is supposed to be in iso8601, but seems like we need to handle simple YYYY-MM-DD as well
73
+ def extract_time(datetime)
74
+ return unless datetime
75
+ Time.parse(datetime).to_i
76
+ end
77
+
78
+ def extract_dates(parent_element, entry)
79
+ datetime = parent_element.at_xpath('./ccr:DateTime')
80
+ if !datetime
81
+ return
82
+ end
83
+ if datetime.at_xpath('./ccr:ExactDateTime')
84
+ entry.time = extract_time(datetime.at_xpath('./ccr:ExactDateTime').content)
85
+ end
86
+ if datetime.at_xpath('./ccr:ApproximateDateTime')
87
+ entry.time = extract_time(datetime.at_xpath('./ccr:ApproximateDateTime').content)
88
+ end
89
+ if datetime.at_xpath('./ccr:DateTimeRange/ccr:BeginRange')
90
+ entry.start_time = extract_time(datetime.at_xpath('./ccr:DateTimeRange/ccr:BeginRange').content)
91
+ end
92
+ if datetime.at_xpath('./ccr:DateTimeRange/ccr:EndRange')
93
+ entry.end_time = extract_time(datetime.at_xpath('./ccr:DateTimeRange/ccr:EndRange').content)
94
+ end
95
+ end
96
+
97
+ def extract_value(parent_element, entry)
98
+ value_element = parent_element.at_xpath('./ccr:TestResult')
99
+ if value_element
100
+ value = value_element.at_xpath('./ccr:Value').content
101
+ unit = value_element.at_xpath('./ccr:Units/ccr:Unit').content
102
+ if value
103
+ entry.set_value(value, unit)
104
+ end
105
+ end
106
+ end # extract_value
107
+
108
+ # Traverses that ASTM CCR document passed in using XPath and creates an Array of Entry
109
+ # objects based on what it finds
110
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
111
+ # will have the "ccr" namespace registered to "urn:astm-org:CCR"
112
+ # measure definition
113
+ # @return [Array] will be a list of Entry objects
114
+ def create_entries(doc)
115
+ return nil
116
+ end
117
+ end
118
+
119
+
120
+ end # Importer
121
+ end
122
+ end # QME
123
+
124
+
@@ -0,0 +1,30 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module CCR
4
+ class SimpleImporter < SectionImporter
5
+ # Traverses that ASTM CCR document passed in using XPath and creates an Array of Entry
6
+ # objects based on what it finds
7
+ # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
8
+ # will have the "ccr" namespace registered to "urn:astm-org:CCR"
9
+ # measure definition
10
+ # @return [Array] will be a list of Entry objects
11
+ def create_entries(doc)
12
+ entry_list = []
13
+ entry_elements = doc.xpath(@entry_xpath)
14
+ entry_elements.each do |entry_element|
15
+ entry = Entry.new
16
+ extract_codes(entry_element, entry)
17
+ extract_dates(entry_element, entry)
18
+ extract_status(entry_element, entry)
19
+ if @check_for_usable
20
+ entry_list << entry if entry.usable?
21
+ else
22
+ entry_list << entry
23
+ end
24
+ end
25
+ entry_list
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module GreenC32
4
+ class ConditionImporter < SectionImporter
5
+ include Singleton
6
+
7
+ def initialize
8
+ super
9
+ end
10
+
11
+ def import(condition_xml)
12
+ condition_xml.root.add_namespace_definition('gc32', "urn:hl7-org:greencda:c32")
13
+
14
+ condition_element = condition_xml.xpath("./gc32:condition")
15
+
16
+ condition = Condition.new
17
+
18
+ extract_entry(condition_element, condition)
19
+ extract_name(condition_element, condition)
20
+ extract_interval(condition_element, condition)
21
+ extract_cause_of_death(condition_element, condition)
22
+ extract_type(condition_element, condition)
23
+ condition
24
+ end
25
+
26
+ private
27
+
28
+ def extract_type(condition_xml, condition)
29
+ type = condition_xml.xpath("./gc32:type").first
30
+ condition.type = extract_node_text(type)
31
+ end
32
+
33
+ def extract_name(condition_xml, condition)
34
+ name = condition_xml.xpath("./gc32:name").first
35
+ condition.name = extract_node_text(name)
36
+ end
37
+
38
+ def extract_cause_of_death(condition_xml, condition)
39
+ condition.cause_of_death = extract_node_attribute(condition_xml, "causeOfDeath")
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module GreenC32
4
+ class PatientImporter
5
+ include Singleton
6
+
7
+ def initialize
8
+ @importers[:results] = ResultImporter.new
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module GreenC32
4
+ class ResultImporter < SectionImporter
5
+ include Singleton
6
+
7
+ def initialize
8
+ super
9
+ @range = "./gc32:referenceRange"
10
+ @interpretation = "./gc32:interpretation"
11
+ end
12
+
13
+ def import(result)
14
+ result.root.add_namespace_definition('gc32', "urn:hl7-org:greencda:c32")
15
+
16
+ result_element = result.xpath("./gc32:result")
17
+ lab_result = LabResult.new(reference_range: extract_node_text(result_element.xpath(@range)))
18
+
19
+ extract_entry(result_element, lab_result)
20
+ extract_time(result_element, lab_result)
21
+ extract_value(result_element, lab_result)
22
+ extract_code(result_element, lab_result, @interpretation, :interpretation)
23
+
24
+ lab_result
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,93 @@
1
+ require "time"
2
+
3
+ module HealthDataStandards
4
+ module Import
5
+ module GreenC32
6
+ class SectionImporter
7
+
8
+ def initialize
9
+ @description = "./gc32:code/gc32:originalText"
10
+ @status = "./gc32:status"
11
+ @value = "./gc32:value"
12
+ end
13
+
14
+ def extract_code(element, entry, xpath="./gc32:code", attribute=:codes)
15
+
16
+ code_element = element.xpath(xpath).first
17
+
18
+ return unless code_element
19
+
20
+ codes = build_code(code_element)
21
+
22
+ code_element.xpath("./gc32:translation").each do |trans|
23
+ codes.merge!(build_code(trans))
24
+ end
25
+
26
+ entry.send("#{attribute}=", codes)
27
+ end
28
+
29
+ def extract_description(element, entry)
30
+ description = element.xpath(@description).first
31
+ entry.description = extract_node_text(description)
32
+ end
33
+
34
+ def extract_status(element, entry)
35
+ status = extract_node_text(element.xpath(@status).first)
36
+ return unless status
37
+ entry.status = status
38
+ end
39
+
40
+ def extract_time(element, entry, xpath = "./gc32:effectiveTime", attribute = "time")
41
+ datetime = element.xpath(xpath).first
42
+ return unless datetime && !datetime.inner_text.empty?
43
+ entry.send("#{attribute}=", Time.parse(datetime.inner_text).to_i)
44
+ end
45
+
46
+ def extract_interval(element, entry)
47
+ extract_time(element, entry, "./gc32:effectiveTime/gc32:start", "start_time")
48
+ extract_time(element, entry, "./gc32:effectiveTime/gc32:end", "end_time")
49
+ end
50
+
51
+ def extract_value(element, entry)
52
+
53
+ value_element = element.xpath(@value).first
54
+
55
+ return unless value_element
56
+
57
+ node_value = extract_node_attribute(value_element, "amount", true)
58
+ node_units = extract_node_attribute(value_element, "unit")
59
+
60
+ entry.value = {'scalar' => node_value, "unit" => node_units} if node_value
61
+
62
+ end
63
+
64
+ def extract_entry(element, entry)
65
+ extract_code(element, entry)
66
+ extract_description(element, entry)
67
+ extract_status(element, entry)
68
+ end
69
+
70
+ private
71
+
72
+ def build_code(code_element)
73
+ code_system_oid = extract_node_attribute(code_element, "codeSystem")
74
+ code = extract_node_attribute(code_element, "code")
75
+ code_system = HealthDataStandards::Util::CodeSystemHelper.code_system_for(code_system_oid)
76
+ {code_system => [code]}
77
+ end
78
+
79
+ def extract_node_attribute(node, attribute_name, to_num=false)
80
+ return if node.nil? || (node.respond_to?(:empty?) && node.empty?)
81
+ attribute = node.attribute(attribute_name.to_s)
82
+ value = attribute ? attribute.value : nil
83
+ return unless value && value != ""
84
+ to_num ? value.to_f : value
85
+ end
86
+
87
+ def extract_node_text(node)
88
+ node ? node.inner_text : nil
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end