quality-measure-engine 1.1.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +12 -0
  2. data/.travis.yml +16 -0
  3. data/Gemfile +5 -21
  4. data/Gemfile.lock +126 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +23 -44
  7. data/Rakefile +6 -29
  8. data/lib/qme/bundle/bundle.rb +34 -0
  9. data/lib/qme/bundle/importer.rb +69 -0
  10. data/lib/qme/database_access.rb +7 -11
  11. data/lib/qme/map/map_reduce_builder.rb +4 -1
  12. data/lib/qme/map/map_reduce_executor.rb +55 -43
  13. data/lib/qme/map/measure_calculation_job.rb +24 -23
  14. data/lib/qme/quality_measure.rb +5 -5
  15. data/lib/qme/quality_report.rb +37 -19
  16. data/lib/qme/railtie.rb +7 -0
  17. data/lib/qme/tasks/bundle.rake +14 -0
  18. data/lib/qme/version.rb +3 -0
  19. data/lib/quality-measure-engine.rb +13 -25
  20. data/quality-measure-engine.gemspec +28 -0
  21. data/test/fixtures/bundles/just_measure_0002.zip +0 -0
  22. data/test/fixtures/delayed_backend_mongoid_jobs/queued_job.json +9 -0
  23. data/test/fixtures/measures/measure_metadata.json +52 -0
  24. data/test/fixtures/records/barry_berry.json +471 -0
  25. data/test/fixtures/records/billy_jones_ipp.json +78 -0
  26. data/test/fixtures/records/jane_jones_numerator.json +120 -0
  27. data/test/fixtures/records/jill_jones_denominator.json +78 -0
  28. data/test/simplecov_setup.rb +18 -0
  29. data/test/test_helper.rb +26 -0
  30. data/test/unit/qme/map/map_reduce_builder_test.rb +38 -0
  31. data/test/unit/qme/map/map_reduce_executor_test.rb +56 -0
  32. data/test/unit/qme/map/measure_calculation_job_test.rb +22 -0
  33. data/test/unit/qme/quality_measure_test.rb +14 -0
  34. data/{spec/qme/quality_report_spec.rb → test/unit/qme/quality_report_test.rb} +32 -20
  35. metadata +91 -115
  36. data/VERSION +0 -1
  37. data/js/map_reduce_utils.js +0 -173
  38. data/js/underscore_min.js +0 -25
  39. data/lib/qme/ext/record.rb +0 -43
  40. data/lib/qme/importer/entry.rb +0 -126
  41. data/lib/qme/importer/generic_importer.rb +0 -117
  42. data/lib/qme/importer/measure_properties_generator.rb +0 -39
  43. data/lib/qme/importer/property_matcher.rb +0 -110
  44. data/lib/qme/measure/database_loader.rb +0 -83
  45. data/lib/qme/measure/measure_loader.rb +0 -174
  46. data/lib/qme/measure/properties_builder.rb +0 -184
  47. data/lib/qme/measure/properties_converter.rb +0 -27
  48. data/lib/qme/randomizer/patient_randomization_job.rb +0 -47
  49. data/lib/qme/randomizer/patient_randomizer.rb +0 -250
  50. data/lib/qme/randomizer/random_patient_creator.rb +0 -47
  51. data/lib/qme_test.rb +0 -13
  52. data/lib/tasks/fixtures.rake +0 -91
  53. data/lib/tasks/measure.rake +0 -110
  54. data/lib/tasks/mongo.rake +0 -68
  55. data/lib/tasks/patient_random.rake +0 -45
  56. data/spec/qme/bundle_spec.rb +0 -37
  57. data/spec/qme/importer/generic_importer_spec.rb +0 -73
  58. data/spec/qme/importer/measure_properties_generator_spec.rb +0 -15
  59. data/spec/qme/importer/property_matcher_spec.rb +0 -174
  60. data/spec/qme/map/map_reduce_builder_spec.rb +0 -38
  61. data/spec/qme/map/measures_spec.rb +0 -38
  62. data/spec/qme/map/patient_mapper_spec.rb +0 -11
  63. data/spec/qme/measure_loader_spec.rb +0 -12
  64. data/spec/qme/properties_builder_spec.rb +0 -61
  65. data/spec/spec_helper.rb +0 -120
  66. data/spec/validate_measures_spec.rb +0 -21
@@ -1,83 +0,0 @@
1
- require File.join(File.dirname(__FILE__),'measure_loader')
2
-
3
- module QME
4
- module Database
5
-
6
- # Utility class for working with JSON files and the database
7
- class Loader
8
- include DatabaseAccess
9
- # Create a new Loader.
10
- # @param [String] db_name the name of the database to use
11
- def initialize(db_name = nil)
12
- determine_connection_information(db_name)
13
- end
14
-
15
- # Load a measure from the filesystem and save it in the database.
16
- # @param [String] measure_dir path to the directory containing a measure or measure collection document
17
- # @param [String] collection_name name of the database collection to save the measure into.
18
- # @return [Array] the stroed measures as an array of JSON measure hashes
19
- def save_measure(measure_dir, collection_name)
20
- measures = QME::Measure::Loader.load_measure(measure_dir)
21
- measures.each do |measure|
22
- save(collection_name, measure)
23
- end
24
- measures
25
- end
26
-
27
- # Save a JSON hash to the specified collection, creates the
28
- # collection if it doesn't already exist.
29
- # @param [String] collection_name name of the database collection
30
- # @param [Hash] json the JSON hash to save in the database
31
- def save(collection_name, json)
32
- get_db[collection_name].save(json)
33
- end
34
-
35
- # Drop a database collection
36
- # @param [String] collection_name name of the database collection
37
- def drop_collection(collection_name)
38
- get_db.drop_collection(collection_name)
39
- end
40
-
41
- def save_system_js_fn(name, fn)
42
- get_db['system.js'].save(
43
- {
44
- "_id" => name,
45
- "value" => BSON::Code.new(fn)
46
- }
47
- )
48
- end
49
-
50
- # Save a bundle to the db, this will save the bundle meta data, javascript extension functions and measures to
51
- # the db in their respective locations
52
- # @param [String] bundle_dir the bundle directory
53
- # @param [String] bundle_collection the collection to save the bundle meta_data and extension functions to
54
- # @param [String] measure_collection the collection to save the measures to, defaults to measures
55
- def save_bundle(bundle_dir, measure_dir, bundle_collection='bundles', measure_collection = 'measures')
56
- bundle = QME::Measure::Loader.load_bundle(bundle_dir, measure_dir)
57
- bundle[:bundle_data][:measures] = []
58
- b_id = save(bundle_collection,bundle[:bundle_data])
59
- measures = bundle[:measures]
60
- measures.each do |measure|
61
- measure[:bundle] = b_id
62
- m_id = save(measure_collection,measure)
63
- bundle[:bundle_data][:measures] << m_id
64
- end
65
- save(bundle_collection,bundle[:bundle_data])
66
- bundle[:extensions].each do |name, fn|
67
- save_system_js_fn(name, fn)
68
- end
69
- bundle
70
- end
71
-
72
-
73
- def remove_bundle(bundle_id, bundle_collection = 'bundles', measure_collection = 'measures')
74
- bundle = get_db[bundle_collection].find_one(bundle_id)
75
- bundle['measures'].each do |measure|
76
- mes = get_db[measure_collection].find_one(measure)
77
- get_db[measure_collection].remove(mes)
78
- end
79
- get_db[bundle_collection].remove(bundle)
80
- end
81
- end
82
- end
83
- end
@@ -1,174 +0,0 @@
1
- gem 'rubyzip'
2
- require 'json'
3
- require 'zip/zip'
4
- require 'zip/zipfilesystem'
5
- require File.join(File.dirname(__FILE__),'properties_builder')
6
-
7
- module QME
8
- module Measure
9
-
10
- # Utility class for working with JSON measure definition files
11
- class Loader
12
-
13
- # Load a measure from the filesystem
14
- # @param [String] measure_dir path to the directory containing a measure or measure collection document
15
- # @return [Array] the measures as an array of JSON measure hashes
16
- def self.load_measure(measure_dir)
17
- measures = []
18
- Dir.glob(File.join(measure_dir, '*.col')).each do |collection_file|
19
- component_dir = File.join(measure_dir, 'components')
20
- load_collection(collection_file, component_dir).each do |measure|
21
- measures << measure
22
- end
23
- end
24
- Dir.glob(File.join(measure_dir, '*.json')).each do |measure_file|
25
- files = Dir.glob(File.join(measure_dir,'*.js'))
26
- if files.length!=1
27
- raise "Unexpected number of map functions in #{measure_dir}, expected 1"
28
- end
29
- map_file = files[0]
30
- measure = load_measure_file(measure_file, map_file)
31
- measures << measure
32
- end
33
- measures
34
- end
35
-
36
- # Load a collection of measures definitions by processing a collection
37
- # definition file.
38
- # @param [String] collection_file path of the collection definition file
39
- # @param [String] component_dir path to the directory that contains the measure components
40
- # @return [Array] an array of measure definition JSON hashes
41
- def self.load_collection(collection_file, component_dir)
42
- collection_def = JSON.parse(File.read(collection_file))
43
- measure_file = File.join(component_dir, collection_def['root'])
44
- measures = []
45
- collection_def['combinations'].each do |combination|
46
- combination['metadata'] ||= {}
47
- if !combination['map_fn']
48
- raise "Missing map function in #{collection_file} (sub_id #{combination['metadata']['sub_id']})"
49
- end
50
- map_file = File.join(component_dir, combination['map_fn'])
51
- measure = load_measure_file(measure_file, map_file)
52
-
53
- # add inline metadata to top level of definition
54
- combination['metadata'].each do |key, value|
55
- measure[key] = value
56
- end
57
- if measure['properties'] && !measure['measure']
58
- # load the measure properties if they weren't already added
59
- measure['measure'] = get_measure_properties(measure)
60
- end
61
-
62
- # add inline measure-specific properties to definition
63
- combination['measure'] ||= {}
64
- measure['measure'] ||= {}
65
- combination['measure'].each do |key, value|
66
- measure['measure'][key] = value
67
- end
68
- ['population', 'denominator', 'numerator', 'exclusions'].each do |component|
69
- if combination[component]
70
- measure[component] = load_json(component_dir, combination[component])
71
- end
72
- end
73
- measures << measure
74
- end
75
- measures
76
- end
77
-
78
- # For ease of development and change mananegment, measure definition JSON
79
- # files, property lists and JavaScript map functions are stored separately
80
- # in the file system, this function combines the three components and
81
- # returns the result.
82
- # @param [String] measure_file path to the measure file
83
- # @param [String] map_fn_file path to the map function file
84
- # @return [Hash] a JSON hash of the measure with embedded map function.
85
- def self.load_measure_file(measure_file, map_fn_file)
86
- map_fn = File.read(map_fn_file)
87
- measure = JSON.parse(File.read(measure_file))
88
- measure['map_fn'] = map_fn
89
- if measure['properties']
90
- measure['measure'] = get_measure_properties(measure)
91
- end
92
- measure
93
- end
94
-
95
- # Load the measure properties from an external file, converting from JSONified XLS if
96
- # necessary
97
- def self.get_measure_properties(measure)
98
- merged_props = {}
99
- # normalize field value to be an array of file names
100
- if !(measure['properties'].respond_to?(:each))
101
- measure['properties'] = [measure['properties']]
102
- end
103
- measure['properties'].each do |file|
104
- measure_props_file = File.join(ENV['MEASURE_PROPS'], file)
105
- measure_props = JSON.parse(File.read(measure_props_file))
106
- if measure_props['measure']
107
- # copy measure properties over
108
- merged_props.merge!(measure_props['measure'])
109
- else
110
- # convert JSONified XLS to properties format and add to measure
111
- converted_props = QME::Measure::PropertiesBuilder.build_properties(measure_props, measure_props_file)['measure']
112
- merged_props.merge!(converted_props)
113
- end
114
- end
115
- merged_props
116
- end
117
-
118
- # Load a JSON file from the specified directory
119
- # @param [String] dir_path path to the directory containing the JSON file
120
- # @param [String] filename the JSON file
121
- # @return [Hash] the parsed JSON hash
122
- def self.load_json(dir_path, filename)
123
- file_path = File.join(dir_path, filename)
124
- JSON.parse(File.read(file_path))
125
- end
126
-
127
-
128
- #Load a bundle from a directory
129
- # @param [String] bundle_path path to directory containing the bundle information
130
- def self.load_bundle(bundle_path, measure_dir)
131
- begin
132
- bundle = {};
133
- bundle_file = File.join(bundle_path,'bundle.json')
134
- license_file = File.join(bundle_path, 'license.html')
135
-
136
- bundle[:bundle_data] = File.exists?(bundle_file) ? JSON.parse(File.read(bundle_file)) : JSON.parse("{}")
137
- bundle[:bundle_data][:license] = File.exists?(license_file) ? File.read(license_file) : ""
138
- bundle[:extensions] = load_bundle_extensions(bundle_path)
139
- bundle[:bundle_data][:extensions] = bundle[:extensions].keys
140
- bundle[:measures] = []
141
- Dir.glob(File.join(bundle_path, measure_dir, '*')).each do |measure_dir|
142
- load_measure(measure_dir).each do |measure|
143
- bundle[:measures] << measure
144
- end
145
- end
146
- bundle
147
- rescue Exception => e
148
- print e.backtrace.join("\n")
149
- throw e
150
- end
151
- end
152
-
153
-
154
- # Load all of the extenson functions that will be available to map reduce functions from the bundle dir
155
- # This will load from bundle_path/js and from ext directories in the individule measures directories
156
- # like bundle_path/measures/0001/ext/
157
- # @param [String] bundle_path the path to the bundle directory
158
- # @return [Hash] name, function
159
- def self.load_bundle_extensions(bundle_path)
160
- extensions = {}
161
- process_extension = lambda do |js_file|
162
- fn_name = File.basename(js_file, ".js")
163
- raw_js = File.read(js_file)
164
- extensions[fn_name] = raw_js
165
- end
166
- Dir.glob(File.join(File.dirname(__FILE__), '../../..', 'js', '*.js')).each &process_extension
167
- Dir.glob(File.join(bundle_path, 'js', '*.js')).each &process_extension
168
- Dir.glob(File.join(bundle_path, 'measures', '*','ext', '*.js')).each &process_extension
169
- extensions
170
- end
171
-
172
- end
173
- end
174
- end
@@ -1,184 +0,0 @@
1
- module QME
2
- module Measure
3
-
4
- # Utility class for converting JSONified XLS file to measure properties
5
- class PropertiesBuilder
6
-
7
- GROUPING = 'GROUPING'
8
- STANDARD_TAXONOMY = 'standard_taxonomy'
9
- STANDARD_CODE_LIST = 'standard_code_list'
10
- STANDARD_CONCEPT = 'standard_concept'
11
- STANDARD_CONCEPT_ID = 'standard_concept_id'
12
- STANDARD_CATEGORY = 'standard_category'
13
- QDS_DATA_TYPE = 'QDS_data_type'
14
- PROPERTIES_TO_IGNORE = %w(birthdate measurement_enddate measurement_period)
15
-
16
- # Convert JSONified measure XLS to measure properties format
17
- def self.build_properties(xls_json, xls_file)
18
- xls_json = patch_properties(xls_json, xls_file)
19
-
20
- # build groupings
21
- grouped_json = build_groups(xls_json)
22
-
23
- properties = {}
24
- grouped_json.each do |value|
25
- property_value = build_property(value)
26
- if !PROPERTIES_TO_IGNORE.include?(property_value[STANDARD_CONCEPT])
27
- properties[property_name(value)] = property_value
28
- end
29
- end
30
-
31
- {'measure' => properties}
32
- end
33
-
34
- # Build the top-level JSON hash for a measure property
35
- def self.build_property(parent)
36
- property = {}
37
- property[STANDARD_CONCEPT] = build_underscore_separated_name(parent[STANDARD_CONCEPT])
38
- property[STANDARD_CATEGORY] = build_underscore_separated_name(parent[STANDARD_CATEGORY])
39
- property[STANDARD_CONCEPT_ID] = parent[STANDARD_CONCEPT_ID]
40
- property['qds_data_type'] = build_underscore_separated_name(parent[QDS_DATA_TYPE])
41
- property['qds_id'] = parent['QDS_id']
42
- property['type'] = 'array'
43
- property['items'] = build_item_def(parent)
44
- property['codes'] = build_code_list(parent)
45
- property
46
- end
47
-
48
- # Build the definition of a property item
49
- def self.build_item_def(property)
50
- if property['value']
51
- {
52
- 'type' => 'object',
53
- 'properties' => {
54
- 'value' => {
55
- 'description' => property['value']['description'],
56
- 'type' => property['value']['type']
57
- },
58
- 'date' => {
59
- 'description' => property['value']['date_description'],
60
- 'type' => 'number',
61
- 'format' => 'utc-sec'
62
- }
63
- }
64
- }
65
- elsif property['range']
66
- {
67
- 'type' => 'object',
68
- 'properties' => {
69
- 'start' => {
70
- 'description' => property['range']['start_description'],
71
- 'type' => 'number',
72
- 'format' => 'utc-sec'
73
- },
74
- 'end' => {
75
- 'description' => property['range']['end_description'],
76
- 'type' => 'number',
77
- 'format' => 'utc-sec'
78
- }
79
- }
80
- }
81
- else
82
- {
83
- 'type' => 'number',
84
- 'format' => 'utc-sec'
85
- }
86
- end
87
- end
88
-
89
- # Build the set of code lists for a property
90
- def self.build_code_list(property)
91
- if property['child_nodes']
92
- property['child_nodes'].collect { |item| build_code_def(item) }
93
- else
94
- [build_code_def(property)]
95
- end
96
- end
97
-
98
- # Build a single code list JSON hash for a property
99
- def self.build_code_def(property)
100
- {
101
- 'set' => property['standard_taxonomy'],
102
- 'version' => property['standard_taxonomy_version'],
103
- STANDARD_CONCEPT => build_underscore_separated_name(property[STANDARD_CONCEPT]),
104
- STANDARD_CONCEPT_ID => property[STANDARD_CONCEPT_ID],
105
- 'qds_id' => property['QDS_id'],
106
- 'values' => extract_code_values(property['standard_code_list'], property['standard_taxonomy'])
107
- }
108
- end
109
-
110
- # Extract the code values from their string form into an array of strings, one per code
111
- def self.extract_code_values(string_list, set)
112
- return [] if string_list==nil || string_list.length==0
113
- string_list.split(',').collect do |entry|
114
- if set=='CPT' && entry.include?('-')
115
- # special handling for ranges in CPT code sets, e.g. 10010-10015
116
- eval(entry.strip.gsub('-','..')).to_a.collect { |i| i.to_s }
117
- else
118
- entry.strip
119
- end
120
- end.flatten
121
- end
122
-
123
- # Generate a property name from the standard concept and qds data type
124
- def self.property_name(property)
125
- build_underscore_separated_name(property[STANDARD_CONCEPT], property[QDS_DATA_TYPE])
126
- end
127
-
128
- # Break all the supplied strings into separate words and return the resulting list as a
129
- # new string with each word separated with '_'
130
- def self.build_underscore_separated_name(*components)
131
- name = []
132
- components.each do |component|
133
- name.concat component.gsub(/\W/,' ').split.collect { |word| word.strip.downcase }
134
- end
135
- name.join '_'
136
- end
137
-
138
- # build an array of grouped properties
139
- def self.build_groups(xls_json)
140
- grouped_json = []
141
- exclude_child_ids = []
142
-
143
- # add grouped members
144
- groups = xls_json.values.select { |value| value[STANDARD_TAXONOMY]!=nil && value[STANDARD_TAXONOMY].upcase==GROUPING }
145
- groups.each do |group|
146
- group_child_ids = group[STANDARD_CODE_LIST].split(',').collect { |id| id.strip }
147
- children = xls_json.values.select do |child|
148
- group_child_ids.include?(child[STANDARD_CONCEPT_ID].strip)
149
- end
150
- group['child_nodes'] = children
151
- grouped_json << group
152
- if group['group_type']==nil || group['group_type']!='abstract'
153
- exclude_child_ids.concat group_child_ids
154
- end
155
- end
156
-
157
- # add remaining non-grouped members
158
- xls_json.values.each do |value|
159
- if value[STANDARD_TAXONOMY]!=GROUPING && !exclude_child_ids.include?(value[STANDARD_CONCEPT_ID].strip)
160
- grouped_json<<value
161
- end
162
- end
163
- grouped_json
164
- end
165
-
166
- # Patch the supplied property_json hash with the contents of a patch file named
167
- # "#{property_file}.patch" and return the result
168
- def self.patch_properties(property_json, property_file)
169
- patch_file = "#{property_file}.patch"
170
-
171
- if File.exists? patch_file
172
- patch_json = JSON.parse(File.read(patch_file))
173
-
174
- property_json = property_json.merge(patch_json) do |key, old, new|
175
- old.merge(new)
176
- end
177
- end
178
- property_json
179
- end
180
-
181
- end
182
-
183
- end
184
- end
@@ -1,27 +0,0 @@
1
- require 'roo'
2
-
3
- module QME
4
- module Measure
5
-
6
- # Utility class for converting NQF XLS files to JSON
7
- class Converter
8
-
9
- #Convert an NQF XLS file to a hash
10
- def self.from_xls(file)
11
- xls =Excelx.new(file)
12
- xls.default_sheet='Measure_QDS'
13
- result = {}
14
- (xls.header_line+1..xls.last_row).each do |row|
15
- entry = {}
16
- (xls.first_column..xls.last_column).each do |column|
17
- entry[xls.cell(xls.header_line, column)] = xls.cell(row, column)
18
- end
19
- result[entry['QDS_id']] = entry
20
- end
21
- result
22
- end
23
-
24
- end
25
-
26
- end
27
- end