bonnie_bundler 2.0.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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +12 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +267 -0
- data/README.md +4 -0
- data/Rakefile +29 -0
- data/bonnie-bundler.gemspec +29 -0
- data/config/initializers/mongo.rb +1 -0
- data/config/measures/measures_2_4_0.yml +719 -0
- data/config/mongoid.yml +6 -0
- data/lib/bonnie_bundler.rb +39 -0
- data/lib/ext/hash.rb +28 -0
- data/lib/ext/railtie.rb +11 -0
- data/lib/ext/valueset.rb +11 -0
- data/lib/measures/cql_to_elm_helper.rb +90 -0
- data/lib/measures/elm_parser.rb +74 -0
- data/lib/measures/loading/base_loader_definition.rb +61 -0
- data/lib/measures/loading/cql_loader.rb +420 -0
- data/lib/measures/loading/exceptions.rb +10 -0
- data/lib/measures/loading/loader.rb +178 -0
- data/lib/measures/loading/value_set_loader.rb +137 -0
- data/lib/measures/logic_extractor.rb +552 -0
- data/lib/measures/mongo_hash_key_wrapper.rb +44 -0
- data/lib/models/cql_measure.rb +160 -0
- data/lib/models/measure.rb +330 -0
- data/test/fixtures/BCS_v5_0_Artifacts.zip +0 -0
- data/test/fixtures/CMS158_v5_4_Artifacts.zip +0 -0
- data/test/fixtures/CMS158_v5_4_Artifacts_Update.zip +0 -0
- data/test/fixtures/DRAFT_CMS2_CQL.zip +0 -0
- data/test/fixtures/bonnienesting01_fixed.zip +0 -0
- data/test/fixtures/vcr_cassettes/mat_5-4_cql_export_vsac_response.yml +4723 -0
- data/test/fixtures/vcr_cassettes/multi_library_webcalls.yml +1892 -0
- data/test/fixtures/vcr_cassettes/valid_translation_response.yml +1120 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response.yml +1678 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_158.yml +1670 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_158_update.yml +1670 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_includes_draft.yml +3480 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_no_profile_version.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_profile.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_verion.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_no_profile_version.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_profile.yml +1196 -0
- data/test/fixtures/vcr_cassettes/vs_loading_version.yml +20331 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts.zip +0 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_Version.zip +0 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_With_Profiles.zip +0 -0
- data/test/simplecov_init.rb +18 -0
- data/test/test_helper.rb +44 -0
- data/test/unit/load_mat_export_test.rb +181 -0
- data/test/unit/measure_complexity_test.rb +32 -0
- data/test/unit/measure_diff_test.rb +68 -0
- data/test/unit/mongo_hash_key_wrapper_test.rb +247 -0
- data/test/unit/storing_mat_export_package_test.rb +45 -0
- data/test/unit/value_set_loading_test.rb +109 -0
- data/test/vcr_setup.rb +20 -0
- metadata +258 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Measures
|
|
2
|
+
# keys can't contain periods https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names
|
|
3
|
+
# technically they can't start with dollar sign either, but that's prohibited for CQL naming
|
|
4
|
+
# so:
|
|
5
|
+
# periods are converted to '^p'
|
|
6
|
+
# and carets are converted to '^c' therefore we aren't invalidating the use of any characters (e.g. caret)
|
|
7
|
+
class MongoHashKeyWrapper
|
|
8
|
+
|
|
9
|
+
def self.wrapKeys(theHash)
|
|
10
|
+
newKeys = Hash.new
|
|
11
|
+
theHash.keys.each do |key|
|
|
12
|
+
if (key.include? '.') || (key.include? '^')
|
|
13
|
+
newKeys[key] = key.gsub(/[\^\.]/, '^' => '^c', '.' => '^p')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
newKeys.each { |old, new| theHash[new] = theHash.delete old}
|
|
17
|
+
# now recurse on any contained hashes
|
|
18
|
+
theHash.each do |key,value|
|
|
19
|
+
if value.respond_to?(:key)
|
|
20
|
+
wrapKeys(value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.unwrapKeys(theHash)
|
|
26
|
+
newKeys = Hash.new
|
|
27
|
+
theHash.keys.each do |key|
|
|
28
|
+
if (key.include? '^p') || (key.include? '^c')
|
|
29
|
+
newKey = key.gsub(/\^p/, '.')
|
|
30
|
+
newKey.gsub!(/\^c/, '^')
|
|
31
|
+
newKeys[key] = newKey
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
newKeys.each { |old, new| theHash[new] = theHash.delete old}
|
|
35
|
+
# now recurse on any contained hashes
|
|
36
|
+
theHash.each do |key,value|
|
|
37
|
+
if value.respond_to?(:key)
|
|
38
|
+
unwrapKeys(value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
class CqlMeasure
|
|
2
|
+
include Mongoid::Document
|
|
3
|
+
include Mongoid::Timestamps
|
|
4
|
+
include Mongoid::Attributes::Dynamic
|
|
5
|
+
|
|
6
|
+
DEFAULT_EFFECTIVE_DATE = Time.gm(2012,12,31,23,59).to_i
|
|
7
|
+
MP_START_DATE = Time.gm(2012,1,1,0,0).to_i
|
|
8
|
+
TYPES = ["ep", "eh"]
|
|
9
|
+
|
|
10
|
+
field :id, type: String
|
|
11
|
+
field :measure_id, type: String
|
|
12
|
+
field :hqmf_id, type: String
|
|
13
|
+
field :hqmf_set_id, type: String
|
|
14
|
+
field :hqmf_version_number, type: Integer
|
|
15
|
+
field :cms_id, type: String
|
|
16
|
+
field :title, type: String, default: ""
|
|
17
|
+
field :description, type: String, default: ""
|
|
18
|
+
field :type, type: String
|
|
19
|
+
field :category, type: String, default: 'uncategorized'
|
|
20
|
+
|
|
21
|
+
field :episode_of_care, type: Boolean
|
|
22
|
+
field :continuous_variable, type: Boolean
|
|
23
|
+
field :episode_ids, type: Array
|
|
24
|
+
|
|
25
|
+
field :needs_finalize, type: Boolean, default: false
|
|
26
|
+
|
|
27
|
+
field :published, type: Boolean
|
|
28
|
+
field :publish_date, type: Date
|
|
29
|
+
field :version, type: Integer
|
|
30
|
+
|
|
31
|
+
field :elm_annotations, type: Hash
|
|
32
|
+
|
|
33
|
+
field :cql, type: Array
|
|
34
|
+
field :elm, type: Array
|
|
35
|
+
field :main_cql_library, type: String
|
|
36
|
+
field :cql_statement_dependencies, type: Hash
|
|
37
|
+
|
|
38
|
+
field :population_criteria, type: Hash
|
|
39
|
+
field :data_criteria, type: Hash
|
|
40
|
+
field :source_data_criteria, type: Hash
|
|
41
|
+
field :measure_period, type: Hash
|
|
42
|
+
field :measure_attributes, type: Array
|
|
43
|
+
field :populations, type: Array
|
|
44
|
+
field :populations_cql_map, type: Hash
|
|
45
|
+
field :observations, type: Array
|
|
46
|
+
|
|
47
|
+
field :value_set_oids, type: Array, default: []
|
|
48
|
+
field :value_set_oid_version_objects, type: Array, default: []
|
|
49
|
+
|
|
50
|
+
field :complexity, type: Hash
|
|
51
|
+
|
|
52
|
+
belongs_to :user
|
|
53
|
+
belongs_to :bundle, class_name: "HealthDataStandards::CQM::Bundle"
|
|
54
|
+
has_and_belongs_to_many :records, :inverse_of => nil
|
|
55
|
+
has_one :package, class_name: "CqlMeasurePackage", inverse_of: :measure, dependent: :delete
|
|
56
|
+
|
|
57
|
+
scope :by_measure_id, ->(id) { where({'measure_id'=>id }) }
|
|
58
|
+
scope :by_type, ->(type) { where({'type'=>type}) }
|
|
59
|
+
scope :by_user, ->(user) { where user_id: user.id }
|
|
60
|
+
|
|
61
|
+
index "user_id" => 1
|
|
62
|
+
# Find the measures matching a patient
|
|
63
|
+
def self.for_patient(record)
|
|
64
|
+
where user_id: record.user_id, hqmf_set_id: { '$in' => record.measure_ids }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def value_sets
|
|
68
|
+
options = { oid: value_set_oids }
|
|
69
|
+
options[:user_id] = user.id if user?
|
|
70
|
+
@value_sets ||= HealthDataStandards::SVS::ValueSet.in(options)
|
|
71
|
+
@value_sets
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns the hqmf-parser's ruby implementation of an HQMF document.
|
|
75
|
+
# Rebuild from population_criteria, data_criteria, and measure_period JSON
|
|
76
|
+
def as_hqmf_model
|
|
77
|
+
json = {
|
|
78
|
+
"id" => self.measure_id,
|
|
79
|
+
"title" => self.title,
|
|
80
|
+
"description" => self.description,
|
|
81
|
+
"population_criteria" => self.population_criteria,
|
|
82
|
+
"data_criteria" => self.data_criteria,
|
|
83
|
+
"source_data_criteria" => self.source_data_criteria,
|
|
84
|
+
"measure_period" => self.measure_period,
|
|
85
|
+
"attributes" => self.measure_attributes,
|
|
86
|
+
"populations" => self.populations,
|
|
87
|
+
"hqmf_id" => self.hqmf_id,
|
|
88
|
+
"hqmf_set_id" => self.hqmf_set_id,
|
|
89
|
+
"hqmf_version_number" => self.hqmf_version_number,
|
|
90
|
+
"cms_id" => self.cms_id
|
|
91
|
+
}
|
|
92
|
+
HQMF::Document.from_json(json)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def all_data_criteria
|
|
96
|
+
as_hqmf_model.all_data_criteria
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Note whether or not the measure is a continuous variable measure.
|
|
100
|
+
before_save :set_continuous_variable
|
|
101
|
+
def set_continuous_variable
|
|
102
|
+
# The return value of this function is not related to whether or not this
|
|
103
|
+
# measure is a CV measure. The true return value ensures false is not
|
|
104
|
+
# accidentally returned here, which would cause the chain of 'before_*' to
|
|
105
|
+
# stop executing.
|
|
106
|
+
self.continuous_variable = populations.map {|x| x.keys}.flatten.uniq.include? HQMF::PopulationCriteria::MSRPOPL
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# When saving calculate the cyclomatic complexity of the measure
|
|
111
|
+
# TODO: Do we want to consider a measure other than "cyclomatic complexity" for CQL?
|
|
112
|
+
# TODO: THIS IS NOT CYCLOMATIC COMPLEXITY, ALL MULTIPLE ELEMENT EXPRESSIONS GET COUNTED AS HIGHER COMPLEXITY, NOT JUST LOGICAL
|
|
113
|
+
before_save :calculate_complexity
|
|
114
|
+
def calculate_complexity
|
|
115
|
+
# We calculate the complexity for each statement, and (at least for now) store the result in the same way
|
|
116
|
+
# we store the complexity for QDM variables
|
|
117
|
+
# TODO: consider whether this is too much of a force fit
|
|
118
|
+
self.complexity = { variables: [] }
|
|
119
|
+
# Recursively look through an expression to count the logical branches
|
|
120
|
+
def count_expression_logical_branches(expression)
|
|
121
|
+
case expression
|
|
122
|
+
when nil
|
|
123
|
+
0
|
|
124
|
+
when Array
|
|
125
|
+
expression.map { |exp| count_expression_logical_branches(exp) }.sum
|
|
126
|
+
when Hash
|
|
127
|
+
case expression['type']
|
|
128
|
+
when 'And', 'Or', 'Not'
|
|
129
|
+
count_expression_logical_branches(expression['operand'])
|
|
130
|
+
when 'Query'
|
|
131
|
+
# TODO: Do we need to look into the source side of the query? Can there be logical operators there?
|
|
132
|
+
count_expression_logical_branches(expression['where']) + count_expression_logical_branches(expression['relationship'])
|
|
133
|
+
else
|
|
134
|
+
1
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
0
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Determine the complexity of each statement
|
|
142
|
+
self.elm.each do |elm|
|
|
143
|
+
if statements = elm.try(:[], 'library').try(:[], 'statements').try(:[], 'def')
|
|
144
|
+
statements.each do |statement|
|
|
145
|
+
self.complexity[:variables] << { name: statement['name'], complexity: count_expression_logical_branches(statement['expression']) }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
self.complexity
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
class CqlMeasurePackage
|
|
155
|
+
include Mongoid::Document
|
|
156
|
+
include Mongoid::Timestamps
|
|
157
|
+
|
|
158
|
+
field :file, type: BSON::Binary
|
|
159
|
+
belongs_to :measure, class_name: "CqlMeasure", inverse_of: :package
|
|
160
|
+
end
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
class Measure
|
|
2
|
+
include Mongoid::Document
|
|
3
|
+
include Mongoid::Timestamps
|
|
4
|
+
include Mongoid::Attributes::Dynamic
|
|
5
|
+
|
|
6
|
+
DEFAULT_EFFECTIVE_DATE = Time.gm(2012,12,31,23,59).to_i
|
|
7
|
+
MP_START_DATE = Time.gm(2012,1,1,0,0).to_i
|
|
8
|
+
TYPES = ["ep", "eh"]
|
|
9
|
+
|
|
10
|
+
store_in collection: 'draft_measures'
|
|
11
|
+
|
|
12
|
+
field :id, type: String
|
|
13
|
+
field :measure_id, type: String
|
|
14
|
+
field :hqmf_id, type: String # should be using this one as primary id!!
|
|
15
|
+
field :hqmf_set_id, type: String
|
|
16
|
+
field :hqmf_version_number, type: Integer
|
|
17
|
+
field :cms_id, type: String
|
|
18
|
+
field :title, type: String
|
|
19
|
+
field :description, type: String
|
|
20
|
+
field :type, type: String
|
|
21
|
+
field :category, type: String, default: 'uncategorized'
|
|
22
|
+
|
|
23
|
+
field :episode_of_care, type: Boolean
|
|
24
|
+
field :continuous_variable, type: Boolean
|
|
25
|
+
field :episode_ids, type: Array # of String ids
|
|
26
|
+
field :custom_functions, type: Hash # stores a custom function for a population criteria (used only in ADE_TTR for observation)
|
|
27
|
+
field :force_sources, type: Array # stores a list of source data criteria to force method creation for (used only in ADE_TTR for LaboratoryTestResultInr)
|
|
28
|
+
|
|
29
|
+
field :needs_finalize, type: Boolean, default: false # if true it indicates that the measure needs to have its episodes or submeasure titles defined
|
|
30
|
+
|
|
31
|
+
field :published, type: Boolean
|
|
32
|
+
field :publish_date, type: Date
|
|
33
|
+
field :version, type: Integer
|
|
34
|
+
|
|
35
|
+
field :population_criteria, type: Hash
|
|
36
|
+
field :data_criteria, type: Hash, default: {}
|
|
37
|
+
field :source_data_criteria, type: Hash, default: {}
|
|
38
|
+
field :measure_period, type: Hash
|
|
39
|
+
field :measure_attributes, type: Array
|
|
40
|
+
field :populations, type: Array
|
|
41
|
+
field :preconditions, type: Hash
|
|
42
|
+
|
|
43
|
+
field :value_set_oids, type: Array, default: []
|
|
44
|
+
|
|
45
|
+
field :map_fns, type: Array, default: []
|
|
46
|
+
|
|
47
|
+
field :complexity, type: Hash
|
|
48
|
+
field :measure_logic, type: Array
|
|
49
|
+
|
|
50
|
+
#make sure that the use has a bundle associated with them
|
|
51
|
+
before_save :set_continuous_variable
|
|
52
|
+
|
|
53
|
+
# Cache the generated JS code, with optional options to manipulate cached result
|
|
54
|
+
def map_fn(population_index, options = {})
|
|
55
|
+
options.assert_valid_keys :clear_db_cache, :cache_result_in_db, :check_crosswalk
|
|
56
|
+
# Defaults are: don't clear the cache, do cache the result in the DB, use user specified crosswalk setting
|
|
57
|
+
options.reverse_merge! clear_db_cache: false, cache_result_in_db: true, check_crosswalk: !!self.user.try(:crosswalk_enabled)
|
|
58
|
+
self.map_fns[population_index] = nil if options[:clear_db_cache]
|
|
59
|
+
self.map_fns[population_index] ||= as_javascript(population_index, options[:check_crosswalk])
|
|
60
|
+
save if changed? && options[:cache_result_in_db]
|
|
61
|
+
self.map_fns[population_index]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Generate and cache all the javascript for the measure, optionally clearing the cache first
|
|
65
|
+
def generate_js(options = {})
|
|
66
|
+
populations.each_with_index { |p, idx| map_fn(idx, options) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Clear any cached JavaScript, forcing it to be generated next time it's requested
|
|
70
|
+
def clear_cached_js
|
|
71
|
+
self.map_fns.map! { nil }
|
|
72
|
+
self.save
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
belongs_to :user
|
|
76
|
+
belongs_to :bundle, class_name: "HealthDataStandards::CQM::Bundle"
|
|
77
|
+
has_and_belongs_to_many :records, :inverse_of => nil
|
|
78
|
+
|
|
79
|
+
scope :by_measure_id, ->(id) { where({'measure_id'=>id }) }
|
|
80
|
+
scope :by_user, ->(user) { where({'user_id'=>user.id}) }
|
|
81
|
+
scope :by_type, ->(type) { where({'type'=>type}) }
|
|
82
|
+
|
|
83
|
+
index "user_id" => 1
|
|
84
|
+
# Find the measures matching a patient
|
|
85
|
+
def self.for_patient(record)
|
|
86
|
+
where user_id: record.user_id, hqmf_set_id: { '$in' => record.measure_ids }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
TYPE_MAP = {
|
|
90
|
+
'problem' => 'conditions',
|
|
91
|
+
'encounter' => 'encounters',
|
|
92
|
+
'labresults' => 'results',
|
|
93
|
+
'procedure' => 'procedures',
|
|
94
|
+
'medication' => 'medications',
|
|
95
|
+
'rx' => 'medications',
|
|
96
|
+
'demographics' => 'characteristic',
|
|
97
|
+
'derived' => 'derived'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Returns the hqmf-parser's ruby implementation of an HQMF document.
|
|
101
|
+
# Rebuild from population_criteria, data_criteria, and measure_period JSON
|
|
102
|
+
def as_hqmf_model
|
|
103
|
+
json = {
|
|
104
|
+
"id" => self.measure_id,
|
|
105
|
+
"title" => self.title,
|
|
106
|
+
"description" => self.description,
|
|
107
|
+
"population_criteria" => self.population_criteria,
|
|
108
|
+
"data_criteria" => self.data_criteria,
|
|
109
|
+
"source_data_criteria" => self.source_data_criteria,
|
|
110
|
+
"measure_period" => self.measure_period,
|
|
111
|
+
"attributes" => self.measure_attributes,
|
|
112
|
+
"populations" => self.populations,
|
|
113
|
+
"hqmf_id" => self.hqmf_id,
|
|
114
|
+
"hqmf_set_id" => self.hqmf_set_id,
|
|
115
|
+
"hqmf_version_number" => self.hqmf_version_number,
|
|
116
|
+
"cms_id" => self.cms_id
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
HQMF::Document.from_json(json)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def value_sets
|
|
123
|
+
options = { oid: value_set_oids }
|
|
124
|
+
options[:user_id] = user.id if user?
|
|
125
|
+
@value_sets ||= HealthDataStandards::SVS::ValueSet.in(options)
|
|
126
|
+
@value_sets
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def all_data_criteria
|
|
130
|
+
as_hqmf_model.all_data_criteria
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def as_javascript(population_index, check_crosswalk=false)
|
|
134
|
+
options = {
|
|
135
|
+
value_sets: value_sets,
|
|
136
|
+
episode_ids: episode_ids,
|
|
137
|
+
continuous_variable: continuous_variable,
|
|
138
|
+
force_sources: force_sources,
|
|
139
|
+
custom_functions: custom_functions,
|
|
140
|
+
check_crosswalk: check_crosswalk
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
HQMF2JS::Generator::Execution.logic(as_hqmf_model, population_index, options)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def set_continuous_variable
|
|
147
|
+
self.continuous_variable = populations.map {|x| x.keys}.flatten.uniq.include? HQMF::PopulationCriteria::MSRPOPL
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
############################## Measure Criteria Keys ##############################
|
|
152
|
+
|
|
153
|
+
# Given a data criteria, return the list of all data criteria keys referenced within, either through
|
|
154
|
+
# children criteria or temporal references; this includes the passed in criteria reference
|
|
155
|
+
def data_criteria_criteria_keys(criteria_reference)
|
|
156
|
+
criteria_keys = [criteria_reference]
|
|
157
|
+
if criteria = self.data_criteria[criteria_reference]
|
|
158
|
+
if criteria['children_criteria'].present?
|
|
159
|
+
criteria_keys.concat(criteria['children_criteria'].map { |c| data_criteria_criteria_keys(c) }.flatten)
|
|
160
|
+
end
|
|
161
|
+
if criteria['temporal_references'].present?
|
|
162
|
+
criteria_keys.concat(criteria['temporal_references'].map { |tr| data_criteria_criteria_keys(tr['reference']) }.flatten)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
criteria_keys
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Given a precondition, return the list of all data criteria keys referenced within
|
|
169
|
+
def precondition_criteria_keys(precondition)
|
|
170
|
+
if precondition['preconditions'] && precondition['preconditions'].size > 0
|
|
171
|
+
precondition['preconditions'].map { |p| precondition_criteria_keys(p) }.flatten
|
|
172
|
+
elsif precondition['reference']
|
|
173
|
+
data_criteria_criteria_keys(precondition['reference'])
|
|
174
|
+
else
|
|
175
|
+
[]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Return the list of all data criteria keys in this measure, indexed by population code
|
|
180
|
+
def criteria_keys_by_population
|
|
181
|
+
criteria_keys_by_population = {}
|
|
182
|
+
population_criteria.each do |name, precondition|
|
|
183
|
+
criteria_keys_by_population[name] = precondition_criteria_keys(precondition).reject { |ck| ck == 'MeasurePeriod' }
|
|
184
|
+
end
|
|
185
|
+
criteria_keys_by_population
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
############################## Measure Complexity Analysis ##############################
|
|
189
|
+
|
|
190
|
+
def precondition_complexity(precondition)
|
|
191
|
+
# We want to calculate the number of branching paths; we can do that by simply counting the leaf nodes.
|
|
192
|
+
# Any children of this particular node can appear either through child preconditions or by reference to a
|
|
193
|
+
# data criteria. ASSERTION: a precondition can never both have child preconditions and a data criteria.
|
|
194
|
+
if precondition['preconditions'] && precondition['preconditions'].size > 0
|
|
195
|
+
precondition['preconditions'].map { |p| precondition_complexity(p) }.sum
|
|
196
|
+
elsif precondition['reference']
|
|
197
|
+
data_criteria_complexity(precondition['reference'])
|
|
198
|
+
else
|
|
199
|
+
1
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def data_criteria_complexity(criteria_reference, options = {})
|
|
204
|
+
options.reverse_merge! calculating_variable: false
|
|
205
|
+
# We want to calculate the number of branching paths, which we can normally do by counting leaf nodes.
|
|
206
|
+
# This is more complicated for data criteria because, in addition to direct children, the criteria can
|
|
207
|
+
# also have temporal references, which can themselves branch. Our approach is to calculate an initial
|
|
208
|
+
# number of leaf nodes through looking at direct children and then seeing if any additional leaves are
|
|
209
|
+
# added through temporal references. A temporal reference that doesn't branch doesn't add a leaf node.
|
|
210
|
+
# Finally, this reference may be a variable, in which case we consider this a leaf node *unless* we are
|
|
211
|
+
# explicitly calculating the complexity of the variable itself
|
|
212
|
+
if criteria = self.data_criteria[criteria_reference]
|
|
213
|
+
complexity = if criteria['children_criteria'].present? && (!criteria['variable'] || options[:calculating_variable])
|
|
214
|
+
criteria['children_criteria'].map { |c| data_criteria_complexity(c) }.sum
|
|
215
|
+
else
|
|
216
|
+
1
|
|
217
|
+
end
|
|
218
|
+
complexity + if criteria['temporal_references'].present?
|
|
219
|
+
criteria['temporal_references'].map { |tr| data_criteria_complexity(tr['reference']) - 1 }.sum
|
|
220
|
+
else
|
|
221
|
+
0
|
|
222
|
+
end
|
|
223
|
+
else
|
|
224
|
+
1
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Calculate the complexity of the measure based on cyclomatic complexity (which for simple logical
|
|
229
|
+
# constructs as used to specify measure populations generally means counting clauses); we calculate
|
|
230
|
+
# the complexity separately for populations and individual variables; this is called when the
|
|
231
|
+
# measure is saved so that the calculated complexity is cached in the DB
|
|
232
|
+
before_save :calculate_complexity
|
|
233
|
+
def calculate_complexity
|
|
234
|
+
self.complexity = { populations: [], variables: [] }
|
|
235
|
+
self.population_criteria.each do |name, precondition|
|
|
236
|
+
complexity = precondition_complexity(precondition)
|
|
237
|
+
self.complexity[:populations] << { name: name, complexity: complexity }
|
|
238
|
+
end
|
|
239
|
+
self.source_data_criteria.each do |reference, criteria|
|
|
240
|
+
next unless criteria['variable']
|
|
241
|
+
name = criteria['description']
|
|
242
|
+
complexity = data_criteria_complexity(reference, calculating_variable: true)
|
|
243
|
+
self.complexity[:variables] << { name: name, complexity: complexity }
|
|
244
|
+
end
|
|
245
|
+
self.complexity
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
#########################################################################################
|
|
249
|
+
|
|
250
|
+
############################## Measure Change Analysis ##############################
|
|
251
|
+
|
|
252
|
+
# Extract the measure logic text; this is also cached in the DB
|
|
253
|
+
before_save :extract_measure_logic
|
|
254
|
+
def extract_measure_logic
|
|
255
|
+
self.measure_logic = []
|
|
256
|
+
# There are occasional issues extracting measure logic; while we want to fix them we also don't want logic
|
|
257
|
+
# extraction issues to hold up loading or updating a measure
|
|
258
|
+
begin
|
|
259
|
+
self.measure_logic.concat HQMF::Measure::LogicExtractor.new().population_logic(self)
|
|
260
|
+
rescue => e
|
|
261
|
+
self.measure_logic << "Error parsing measure logic: #{e.message}"
|
|
262
|
+
end
|
|
263
|
+
self.measure_logic
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Compute a simplified diff hash for Complexity Dashboard usage; stored within measure.latest_diff
|
|
267
|
+
def diff(other)
|
|
268
|
+
HQMF::Measure::LogicExtractor.get_measure_logic_diff(self,other,true)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
#########################################################################################
|
|
272
|
+
|
|
273
|
+
def measure_json(population_index=0,check_crosswalk=false)
|
|
274
|
+
options = {
|
|
275
|
+
value_sets: value_sets,
|
|
276
|
+
episode_ids: episode_ids,
|
|
277
|
+
continuous_variable: continuous_variable,
|
|
278
|
+
force_sources: force_sources,
|
|
279
|
+
custom_functions: custom_functions,
|
|
280
|
+
check_crosswalk: check_crosswalk
|
|
281
|
+
}
|
|
282
|
+
population_index ||= 0
|
|
283
|
+
json = {
|
|
284
|
+
id: self.hqmf_id,
|
|
285
|
+
nqf_id: self.measure_id,
|
|
286
|
+
hqmf_id: self.hqmf_id,
|
|
287
|
+
hqmf_set_id: self.hqmf_set_id,
|
|
288
|
+
hqmf_version_number: self.hqmf_version_number,
|
|
289
|
+
cms_id: self.cms_id,
|
|
290
|
+
name: self.title,
|
|
291
|
+
description: self.description,
|
|
292
|
+
type: self.type,
|
|
293
|
+
category: self.category,
|
|
294
|
+
map_fn: HQMF2JS::Generator::Execution.measure_js(self.as_hqmf_model, population_index, options),
|
|
295
|
+
continuous_variable: self.continuous_variable,
|
|
296
|
+
episode_of_care: self.episode_of_care,
|
|
297
|
+
hqmf_document: self.as_hqmf_model.to_json
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (self.populations.count > 1)
|
|
301
|
+
sub_ids = ('a'..'az').to_a
|
|
302
|
+
json[:sub_id] = sub_ids[population_index]
|
|
303
|
+
population_title = self.populations[population_index]['title']
|
|
304
|
+
json[:subtitle] = population_title
|
|
305
|
+
json[:short_subtitle] = population_title
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
if self.continuous_variable
|
|
309
|
+
observation = self.population_criteria[self.populations[population_index][HQMF::PopulationCriteria::OBSERV]]
|
|
310
|
+
json[:aggregator] = observation['aggregator']
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
json[:oids] = self.value_sets.map{|value_set| value_set.oid}.uniq
|
|
314
|
+
|
|
315
|
+
population_ids = {}
|
|
316
|
+
HQMF::PopulationCriteria::ALL_POPULATION_CODES.each do |type|
|
|
317
|
+
population_key = self.populations[population_index][type]
|
|
318
|
+
population_criteria = self.population_criteria[population_key]
|
|
319
|
+
if (population_criteria)
|
|
320
|
+
population_ids[type] = population_criteria['hqmf_id']
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
stratification = self['populations'][population_index]['stratification']
|
|
324
|
+
if stratification
|
|
325
|
+
population_ids['stratification'] = stratification
|
|
326
|
+
end
|
|
327
|
+
json[:population_ids] = population_ids
|
|
328
|
+
json
|
|
329
|
+
end
|
|
330
|
+
end
|