quality-measure-engine 1.1.5 → 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.
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