health-data-standards 1.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Gemfile +5 -5
  2. data/lib/health-data-standards.rb +8 -0
  3. data/lib/health-data-standards/export/ccda.rb +15 -0
  4. data/lib/health-data-standards/export/ccr.rb +24 -20
  5. data/lib/health-data-standards/export/html.rb +4 -8
  6. data/lib/health-data-standards/export/template_helper.rb +7 -1
  7. data/lib/health-data-standards/export/view_helper.rb +24 -4
  8. data/lib/health-data-standards/import/c32/allergy_importer.rb +2 -1
  9. data/lib/health-data-standards/import/c32/care_goal_importer.rb +2 -1
  10. data/lib/health-data-standards/import/c32/condition_importer.rb +23 -2
  11. data/lib/health-data-standards/import/c32/encounter_importer.rb +6 -5
  12. data/lib/health-data-standards/import/c32/immunization_importer.rb +1 -15
  13. data/lib/health-data-standards/import/c32/medication_importer.rb +1 -0
  14. data/lib/health-data-standards/import/c32/patient_importer.rb +13 -0
  15. data/lib/health-data-standards/import/c32/procedure_importer.rb +1 -0
  16. data/lib/health-data-standards/import/c32/provider_importer.rb +1 -1
  17. data/lib/health-data-standards/import/c32/result_importer.rb +1 -0
  18. data/lib/health-data-standards/import/c32/section_importer.rb +17 -22
  19. data/lib/health-data-standards/import/ccda/medical_equipment_importer.rb +1 -2
  20. data/lib/health-data-standards/import/ccda/result_importer.rb +1 -0
  21. data/lib/health-data-standards/import/ccr/provider_importer.rb +0 -1
  22. data/lib/health-data-standards/import/green_c32/immunization_importer.rb +1 -1
  23. data/lib/health-data-standards/import/green_c32/section_importer.rb +7 -5
  24. data/lib/health-data-standards/import/provider_import_utils.rb +1 -1
  25. data/lib/health-data-standards/models/coded_result_value.rb +3 -0
  26. data/lib/health-data-standards/models/condition.rb +7 -5
  27. data/lib/health-data-standards/models/encounter.rb +5 -2
  28. data/lib/health-data-standards/models/entry.rb +48 -68
  29. data/lib/health-data-standards/models/facility.rb +9 -0
  30. data/lib/health-data-standards/models/functional_status.rb +25 -0
  31. data/lib/health-data-standards/models/immunization.rb +4 -6
  32. data/lib/health-data-standards/models/medication.rb +6 -0
  33. data/lib/health-data-standards/models/physical_quantity_result_value.rb +4 -0
  34. data/lib/health-data-standards/models/record.rb +11 -2
  35. data/lib/health-data-standards/models/result_value.rb +4 -0
  36. data/lib/health-data-standards/models/thing_with_codes.rb +52 -0
  37. data/lib/health-data-standards/util/code_system_helper.rb +5 -2
  38. data/lib/health-data-standards/util/qrda_template_helper.rb +20 -0
  39. data/templates/_allergies_no_current.c32.erb +26 -4
  40. data/templates/_conditions_no_current.c32.erb +1 -2
  41. data/templates/_medications_no_current.c32.erb +1 -1
  42. data/templates/_narrative_block.c32.erb +14 -2
  43. data/templates/_result.gc32.erb +3 -1
  44. data/templates/_results.c32.erb +9 -8
  45. data/templates/_social_history.gc32.erb +1 -1
  46. data/templates/_vital_sign.gc32.erb +1 -1
  47. data/templates/_vital_signs.c32.erb +12 -11
  48. data/templates/show.c32.erb +1 -1
  49. metadata +55 -21
  50. data/templates/show.html.erb +0 -287
@@ -4,10 +4,9 @@ module HealthDataStandards
4
4
  class MedicalEquipmentImporter < C32::MedicalEquipmentImporter
5
5
 
6
6
  def initialize
7
- @entry_xpath = "//cda:section[cda:templateId/@root='2.16.840.1.113883.10.20.22.4.50']/cda:entry/cda:supply"
7
+ @entry_xpath = "//cda:section[cda:templateId/@root='2.16.840.1.113883.10.20.22.2.23']/cda:entry/cda:supply"
8
8
  @code_xpath = "./cda:participant/cda:participantRole/cda:playingDevice/cda:code"
9
9
  @description_xpath = "./cda:code/cda:originalText/cda:reference[@value] | ./cda:text/cda:reference[@value]"
10
-
11
10
  end
12
11
 
13
12
  end
@@ -6,6 +6,7 @@ module HealthDataStandards
6
6
  @entry_xpath = "//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.22.4.2']"
7
7
  @code_xpath = "./cda:code"
8
8
  @description_xpath = "./cda:code/cda:originalText/cda:reference[@value] | ./cda:text/cda:reference[@value] "
9
+ @check_for_usable = true
9
10
  end
10
11
  end
11
12
  end
@@ -33,7 +33,6 @@ module HealthDataStandards
33
33
  provider[:npi] = npi if Provider.valid_npi?(npi)
34
34
  end
35
35
 
36
- # binding.pry
37
36
  find_or_create_provider(provider)
38
37
  end
39
38
 
@@ -13,7 +13,7 @@ module HealthDataStandards
13
13
  extract_code(immunization_element, immunization, "./gc32:refusalReason")
14
14
  series_number = extract_node_text(immunization_element.at_xpath("./gc32:seriesNumber"))
15
15
  immunization.series_number = series_number.to_i if series_number
16
- immunization.refusalInd = extract_node_attribute(immunization_element, :refused, true)
16
+ immunization.refusal_ind = extract_node_attribute(immunization_element, :refused, true)
17
17
  immunization
18
18
  end
19
19
 
@@ -71,7 +71,7 @@ module HealthDataStandards
71
71
 
72
72
  return unless datetime && datetime['value']
73
73
 
74
- entry.send("#{attribute}=", Time.parse(datetime['value']).to_i)
74
+ entry.send("#{attribute}=", Time.parse(datetime['value']).utc.to_i)
75
75
  end
76
76
 
77
77
  def extract_interval(element, entry, element_name="effectiveTime")
@@ -89,11 +89,14 @@ module HealthDataStandards
89
89
 
90
90
  return {} unless node_value
91
91
 
92
- {'scalar' => node_value, "unit" => node_units}
92
+ {"scalar" => node_value, "unit" => node_units}
93
93
  end
94
94
 
95
95
  def extract_value(element, entry)
96
- entry.value = extract_quantity(element, @value)
96
+ pq = extract_quantity(element, @value)
97
+ if pq.present?
98
+ entry.values << PhysicalQuantityResultValue.new(pq)
99
+ end
97
100
  end
98
101
 
99
102
  def extract_entry(element, entry)
@@ -142,8 +145,7 @@ module HealthDataStandards
142
145
  def extract_free_text(element, entry, free_text_element="freeText")
143
146
  entry.free_text = extract_node_text(element.at_xpath("./gc32:#{free_text_element}"))
144
147
  end
145
-
146
-
148
+
147
149
  private
148
150
 
149
151
  def build_code(code_element)
@@ -6,7 +6,7 @@ module ProviderImportUtils
6
6
  end
7
7
 
8
8
  def find_or_create_provider(provider_hash)
9
- provider = Provider.first(conditions: {npi: provider_hash[:npi]}) if provider_hash[:npi] && !provider_hash[:npi].empty?
9
+ provider = Provider.where(npi: provider_hash[:npi]).first if provider_hash[:npi] && !provider_hash[:npi].empty?
10
10
  provider ||= Provider.create(provider_hash)
11
11
  end
12
12
 
@@ -0,0 +1,3 @@
1
+ class CodedResultValue < ResultValue
2
+ include ThingWithCodes
3
+ end
@@ -1,9 +1,11 @@
1
1
  class Condition < Entry
2
- field :type, type: String
3
- field :causeOfDeath, type: Boolean
4
- field :priority, type: Integer
5
- field :name, type: String
6
- field :ordinality, type: String
2
+ field :type, type: String
3
+ field :causeOfDeath, type: Boolean
4
+ field :priority, type: Integer
5
+ field :name, type: String
6
+ field :ordinality, type: String
7
+ field :ordinality_code, type: Hash
8
+ field :severity, type: Hash # Currently unsupported by any importers
7
9
 
8
10
  embeds_many :treating_provider, class_name: "Provider"
9
11
 
@@ -1,8 +1,11 @@
1
1
  class Encounter < Entry
2
+
2
3
  field :admitType, type: Hash
3
4
  field :dischargeDisposition, type: Hash
4
-
5
- embeds_one :facility, class_name: "Organization"
5
+ field :admit_time, type: Integer
6
+ field :discharge_time, type: Integer
7
+
8
+ embeds_one :facility
6
9
  embeds_one :reason, class_name: "Entry"
7
10
 
8
11
  belongs_to :performer, class_name: "Provider"
@@ -1,61 +1,35 @@
1
1
  class Entry
2
2
 
3
3
  include Mongoid::Document
4
+ include ThingWithCodes
4
5
 
5
6
  # embedded_in :entry_list, polymorphic: true
6
7
 
7
8
  embedded_in :record
8
9
 
10
+ embeds_many :values, class_name: "ResultValue"
11
+
9
12
  field :description, type: String
10
13
  field :specifics, type: String
11
14
  field :time, type: Integer
12
15
  field :start_time, type: Integer
13
16
  field :end_time, type: Integer
14
- field :status, type: String
15
- field :codes, type: Hash, default: {}
16
- field :value, type: Hash, default: {}
17
+ field :status_code, type: Hash
17
18
  field :free_text, type: String
18
19
  field :mood_code, type: String, default: "EVN"
20
+ field :negationInd, type: Boolean
21
+ field :negationReason, type: Hash
22
+ field :oid, type: String
23
+
24
+ alias :negation_ind :negationInd
25
+ alias :negation_ind= :negationInd=
26
+ alias :negation_reason :negationReason
27
+ alias :negation_reason= :negationReason=
19
28
 
20
29
  attr_protected :version
21
30
  attr_protected :_id
22
31
  attr_protected :created_at
23
32
  attr_protected :updated_at
24
-
25
- def single_code_value?
26
- codes.size == 1 && codes.first[1].size == 1
27
- end
28
-
29
- def codes_to_s
30
- codes.map {|code_set, codes| "#{code_set}: #{codes.join(', ')}"}.join(' ')
31
- end
32
-
33
- # Will return a single code and code set if one exists in the code sets that are
34
- # passed in. Returns a hash with a key of code and code_set if found, nil otherwise
35
- def preferred_code(preferred_code_sets, codes_attribute=:codes)
36
- codes_value = send(codes_attribute)
37
- matching_code_sets = preferred_code_sets & codes_value.keys
38
- if matching_code_sets.present?
39
- code_set = matching_code_sets.first
40
- {'code' => codes_value[code_set].first, 'code_set' => code_set}
41
- else
42
- nil
43
- end
44
- end
45
-
46
- # Will return an Array of code and code_set hashes for all codes for this entry
47
- # except for the preferred_code. It is intended that these codes would be used in
48
- # the translation elements as childern of a CDA code element
49
- def translation_codes(preferred_code_sets)
50
- tx_codes = []
51
- codes.each_pair do |code_set, code_list|
52
- code_list.each do |code|
53
- tx_codes << {'code' => code, 'code_set' => code_set}
54
- end
55
- end
56
-
57
- tx_codes - [preferred_code(preferred_code_sets)]
58
- end
59
33
 
60
34
  def times_to_s
61
35
  if start_time.present? || end_time.present?
@@ -67,24 +41,38 @@ class Entry
67
41
  end
68
42
  end
69
43
 
70
- # def to_effective_time(xml)
71
- # if time.present?
72
- # xml.effectiveTime("value" => Time.at(time).utc.to_formatted_s(:number))
73
- # else
74
- # xml.effectiveTime do
75
- # if start_time.present?
76
- # xml.low("value" => Time.at(start_time).utc.to_formatted_s(:number))
77
- # else
78
- # xml.low("nullFlavor" => "UNK")
79
- # end
80
- # if end_time.present?
81
- # xml.high("value" => Time.at(end_time).utc.to_formatted_s(:number))
82
- # else
83
- # xml.high("nullFlavor" => "UNK")
84
- # end
85
- # end
86
- # end
87
- # end
44
+ # Entry previously had a status field that dropped the code set and converted
45
+ # the status to a String. Entry now preserves the original code and code set.
46
+ # This method is here to maintain backwards compatibility.
47
+ def status
48
+ if status_code.present?
49
+ if status_code['HL7 ActStatus']
50
+ status_code['HL7 ActStatus'].first()
51
+ elsif status_code['SNOMED-CT']
52
+ case status_code['SNOMED-CT'].first()
53
+ when '55561003'
54
+ 'active'
55
+ when '73425007'
56
+ 'inactive'
57
+ when '413322009'
58
+ 'resolved'
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def status=(status_text)
65
+ case status_text.to_s # makes sure that any Symbols passed in are stringified
66
+ when 'active'
67
+ self.status_code = {'SNOMED-CT' => ['55561003'], 'HL7 ActStatus' => ['active']}
68
+ when 'inactive'
69
+ self.status_code = {'SNOMED-CT' => ['73425007']}
70
+ when 'resolved'
71
+ self.status_code = {'SNOMED-CT' => ['413322009']}
72
+ when 'completed'
73
+ self.status_code = {'HL7 ActStatus' => ['completed']}
74
+ end
75
+ end
88
76
 
89
77
  def self.from_event_hash(event)
90
78
  entry = Entry.new
@@ -107,20 +95,12 @@ class Entry
107
95
  entry
108
96
  end
109
97
 
110
- # Add a code into the Entry
111
- # @param [String] code the code to add
112
- # @param [String] code_system the code system that the code belongs to
113
- def add_code(code, code_system)
114
- self.codes[code_system] ||= []
115
- self.codes[code_system] << code
116
- end
117
-
118
98
  # Sets the value for the entry
119
99
  # @param [String] scalar the value
120
100
  # @param [String] units the units of the scalar value
121
101
  def set_value(scalar, units=nil)
122
- self.value[:scalar] = scalar
123
- self.value[:units] = units
102
+ pq_value = PhysicalQuantityResultValue.new(scalar: scalar, units: units)
103
+ self.values << pq_value
124
104
  end
125
105
 
126
106
  # Checks if a code is in the list of possible codes
@@ -186,8 +166,8 @@ class Entry
186
166
  def to_hash
187
167
  entry_hash = {}
188
168
  entry_hash['codes'] = codes
189
- unless value.empty?
190
- entry_hash['value'] = value
169
+ unless values.empty?
170
+ entry_hash['value'] = values
191
171
  end
192
172
 
193
173
  if is_date_range?
@@ -0,0 +1,9 @@
1
+ class Facility
2
+ include Mongoid::Document
3
+
4
+ field :name, type: String
5
+ field :code, type: Hash
6
+
7
+ embeds_many :addresses, as: :locatable
8
+ embeds_many :telecoms, as: :contactable
9
+ end
@@ -0,0 +1,25 @@
1
+ # This class can be used to represnt a functional status for a patient. Currently,
2
+ # it is not a very close representation of functional status as it is represented
3
+ # in the HL7 CCD, HITSP C32 or Consolidated CDA.
4
+ #
5
+ # In the previously mentioned specifications, functional status may represented
6
+ # using either a condition or result. Having "mixed" types of entries in a section
7
+ # is currently not well supported in the existing Record class
8
+ #
9
+ # Additionally, there is a mismatch between the data needed to calculate Stage 2
10
+ # Meaningful Use Quailty Measures and the data contained in patient summary
11
+ # standards. The CQMs are checking to see if a functional status represented by
12
+ # a result was patient supplied. Right now, results do not have a source, and
13
+ # even if we were to use Provider as a source, it would need to be extended
14
+ # to support patients.
15
+ #
16
+ # To avoid this, the patient sumamry style functional status has been "flattened"
17
+ # into this class. This model supports the information needed to calculate
18
+ # Stage 2 MU CQMs. If importers are created from C32 or CCDA, the information
19
+ # can be stored here, but it will be a lossy transformation.
20
+ class FunctionalStatus < Entry
21
+ # Either "condition" or "result"
22
+ field :type, type: String
23
+ # A coded value. Like a code for patient supplied.
24
+ field :source, type: Hash
25
+ end
@@ -1,16 +1,14 @@
1
1
  class Immunization < Entry
2
- field :refusalInd, type: Boolean
3
- field :refusalReason, type: Hash
4
2
  field :seriesNumber, type: Integer
5
3
 
6
4
  belongs_to :performer, class_name: "Provider"
7
5
 
8
6
  embeds_one :medication_product
9
7
 
10
- alias :refusal_ind :refusalInd
11
- alias :refusal_ind= :refusalInd=
12
- alias :refusal_reason :refusalReason
13
- alias :refusal_reason= :refusalReason=
8
+ alias :refusal_ind :negationInd
9
+ alias :refusal_ind= :negationInd=
10
+ alias :refusal_reason :negationReason
11
+ alias :refusal_reason= :negationReason=
14
12
  alias :series_number :seriesNumber
15
13
  alias :series_number= :seriesNumber=
16
14
  end
@@ -18,6 +18,10 @@ class Medication < Entry
18
18
  field :deliveryMethod, type: Hash
19
19
  field :patientInstructions, type: String
20
20
  field :doseIndicator, type: String
21
+
22
+ # There are currently no importers that support this field
23
+ # It is expected to be a scalar and value, such as 7 days
24
+ field :cumulativeMedicationDuration, type: Hash
21
25
 
22
26
  alias :administration_timing :administrationTiming
23
27
  alias :administration_timing= :administrationTiming=
@@ -41,4 +45,6 @@ class Medication < Entry
41
45
  alias :patient_instructions= :patientInstructions=
42
46
  alias :dose_indicator :doseIndicator
43
47
  alias :dose_indicator= :doseIndicator=
48
+ alias :cumulative_medication_duration :cumulativeMedicationDuration
49
+ alias :cumulative_medication_duration= :cumulativeMedicationDuration=
44
50
  end
@@ -0,0 +1,4 @@
1
+ class PhysicalQuantityResultValue < ResultValue
2
+ field :scalar
3
+ field :units, type: String
4
+ end
@@ -12,9 +12,13 @@ class Record
12
12
  field :race, type: Hash
13
13
  field :ethnicity, type: Hash
14
14
  field :languages, type: Array
15
- field :test_id, type: BSON::ObjectId
15
+ field :test_id, type: Moped::BSON::ObjectId
16
16
  field :marital_status, type: Hash # TODO
17
17
  field :medical_record_number, type: String
18
+ field :expired, type: Boolean
19
+ field :clinicalTrialParticipant, type: Boolean # Currently not implemented in the C32 importer
20
+ # because it cannot be easily represented in a
21
+ # HITSP C32
18
22
 
19
23
  embeds_many :allergies
20
24
  embeds_many :care_goals, class_name: "Entry" # This can be any number of different entry types
@@ -30,9 +34,11 @@ class Record
30
34
  embeds_many :support
31
35
  embeds_many :advance_directives, class_name: "Entry"
32
36
  embeds_many :insurance_providers
37
+ embeds_many :functional_statuses
33
38
 
34
39
  Sections = [:allergies, :care_goals, :conditions, :encounters, :immunizations, :medical_equipment,
35
- :medications, :procedures, :results, :social_history, :vital_signs, :support, :advanced_directives]
40
+ :medications, :procedures, :results, :social_history, :vital_signs, :support, :advanced_directives,
41
+ :functional_statuses]
36
42
 
37
43
  embeds_many :provider_performances
38
44
 
@@ -47,6 +53,9 @@ class Record
47
53
  Time.at(birthdate) < Time.now.years_ago(18)
48
54
  end
49
55
 
56
+ alias :clinical_trial_participant :clinicalTrialParticipant
57
+ alias :clinical_trial_participant= :clinicalTrialParticipant=
58
+
50
59
  private
51
60
 
52
61
  def self.provider_queries(provider_id, effective_date)
@@ -0,0 +1,4 @@
1
+ class ResultValue
2
+ include Mongoid::Document
3
+ embedded_in :entry
4
+ end
@@ -0,0 +1,52 @@
1
+ module ThingWithCodes
2
+ def self.included(receiver)
3
+ receiver.field :codes, type: Hash, default: {}
4
+ end
5
+
6
+ def single_code_value?
7
+ codes.size == 1 && codes.first[1].size == 1
8
+ end
9
+
10
+ def codes_to_s
11
+ ThingWithCodes.convert_codes_to_s(codes)
12
+ end
13
+
14
+ def self.convert_codes_to_s(codes)
15
+ codes.map {|code_set, codes| "#{code_set}: #{codes.join(', ')}"}.join(' ')
16
+ end
17
+
18
+ # Will return a single code and code set if one exists in the code sets that are
19
+ # passed in. Returns a hash with a key of code and code_set if found, nil otherwise
20
+ def preferred_code(preferred_code_sets, codes_attribute=:codes)
21
+ codes_value = send(codes_attribute)
22
+ matching_code_sets = preferred_code_sets & codes_value.keys
23
+ if matching_code_sets.present?
24
+ code_set = matching_code_sets.first
25
+ {'code' => codes_value[code_set].first, 'code_set' => code_set}
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ # Will return an Array of code and code_set hashes for all codes for this entry
32
+ # except for the preferred_code. It is intended that these codes would be used in
33
+ # the translation elements as childern of a CDA code element
34
+ def translation_codes(preferred_code_sets)
35
+ tx_codes = []
36
+ codes.each_pair do |code_set, code_list|
37
+ code_list.each do |code|
38
+ tx_codes << {'code' => code, 'code_set' => code_set}
39
+ end
40
+ end
41
+
42
+ tx_codes - [preferred_code(preferred_code_sets)]
43
+ end
44
+
45
+ # Add a code into the Entry
46
+ # @param [String] code the code to add
47
+ # @param [String] code_system the code system that the code belongs to
48
+ def add_code(code, code_system)
49
+ self.codes[code_system] ||= []
50
+ self.codes[code_system] << code
51
+ end
52
+ end