health-data-standards 0.7.0 → 0.7.1

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 (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