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.
- data/.gitignore +12 -0
- data/.travis.yml +16 -0
- data/Gemfile +5 -21
- data/Gemfile.lock +126 -0
- data/LICENSE.txt +13 -0
- data/README.md +23 -44
- data/Rakefile +6 -29
- data/lib/qme/bundle/bundle.rb +34 -0
- data/lib/qme/bundle/importer.rb +69 -0
- data/lib/qme/database_access.rb +7 -11
- data/lib/qme/map/map_reduce_builder.rb +4 -1
- data/lib/qme/map/map_reduce_executor.rb +55 -43
- data/lib/qme/map/measure_calculation_job.rb +24 -23
- data/lib/qme/quality_measure.rb +5 -5
- data/lib/qme/quality_report.rb +37 -19
- data/lib/qme/railtie.rb +7 -0
- data/lib/qme/tasks/bundle.rake +14 -0
- data/lib/qme/version.rb +3 -0
- data/lib/quality-measure-engine.rb +13 -25
- data/quality-measure-engine.gemspec +28 -0
- data/test/fixtures/bundles/just_measure_0002.zip +0 -0
- data/test/fixtures/delayed_backend_mongoid_jobs/queued_job.json +9 -0
- data/test/fixtures/measures/measure_metadata.json +52 -0
- data/test/fixtures/records/barry_berry.json +471 -0
- data/test/fixtures/records/billy_jones_ipp.json +78 -0
- data/test/fixtures/records/jane_jones_numerator.json +120 -0
- data/test/fixtures/records/jill_jones_denominator.json +78 -0
- data/test/simplecov_setup.rb +18 -0
- data/test/test_helper.rb +26 -0
- data/test/unit/qme/map/map_reduce_builder_test.rb +38 -0
- data/test/unit/qme/map/map_reduce_executor_test.rb +56 -0
- data/test/unit/qme/map/measure_calculation_job_test.rb +22 -0
- data/test/unit/qme/quality_measure_test.rb +14 -0
- data/{spec/qme/quality_report_spec.rb → test/unit/qme/quality_report_test.rb} +32 -20
- metadata +91 -115
- data/VERSION +0 -1
- data/js/map_reduce_utils.js +0 -173
- data/js/underscore_min.js +0 -25
- data/lib/qme/ext/record.rb +0 -43
- data/lib/qme/importer/entry.rb +0 -126
- data/lib/qme/importer/generic_importer.rb +0 -117
- data/lib/qme/importer/measure_properties_generator.rb +0 -39
- data/lib/qme/importer/property_matcher.rb +0 -110
- data/lib/qme/measure/database_loader.rb +0 -83
- data/lib/qme/measure/measure_loader.rb +0 -174
- data/lib/qme/measure/properties_builder.rb +0 -184
- data/lib/qme/measure/properties_converter.rb +0 -27
- data/lib/qme/randomizer/patient_randomization_job.rb +0 -47
- data/lib/qme/randomizer/patient_randomizer.rb +0 -250
- data/lib/qme/randomizer/random_patient_creator.rb +0 -47
- data/lib/qme_test.rb +0 -13
- data/lib/tasks/fixtures.rake +0 -91
- data/lib/tasks/measure.rake +0 -110
- data/lib/tasks/mongo.rake +0 -68
- data/lib/tasks/patient_random.rake +0 -45
- data/spec/qme/bundle_spec.rb +0 -37
- data/spec/qme/importer/generic_importer_spec.rb +0 -73
- data/spec/qme/importer/measure_properties_generator_spec.rb +0 -15
- data/spec/qme/importer/property_matcher_spec.rb +0 -174
- data/spec/qme/map/map_reduce_builder_spec.rb +0 -38
- data/spec/qme/map/measures_spec.rb +0 -38
- data/spec/qme/map/patient_mapper_spec.rb +0 -11
- data/spec/qme/measure_loader_spec.rb +0 -12
- data/spec/qme/properties_builder_spec.rb +0 -61
- data/spec/spec_helper.rb +0 -120
- data/spec/validate_measures_spec.rb +0 -21
data/js/underscore_min.js
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
function(){
|
2
|
-
// Underscore.js 1.1.2
|
3
|
-
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
|
4
|
-
// Underscore is freely distributable under the MIT license.
|
5
|
-
// Portions of Underscore are inspired or borrowed from Prototype,
|
6
|
-
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
7
|
-
// For all details and documentation:
|
8
|
-
// http://documentcloud.github.com/underscore
|
9
|
-
var o=this,A=o._,r=typeof StopIteration!=="undefined"?StopIteration:"__break__",k=Array.prototype,m=Object.prototype,i=k.slice,B=k.unshift,C=m.toString,p=m.hasOwnProperty,s=k.forEach,t=k.map,u=k.reduce,v=k.reduceRight,w=k.filter,x=k.every,y=k.some,n=k.indexOf,z=k.lastIndexOf;m=Array.isArray;var D=Object.keys,c=function(a){return new l(a)};if(typeof exports!=="undefined")exports._=c;o._=c;c.VERSION="1.1.2";var j=c.each=c.forEach=function(a,b,d){try{if(s&&a.forEach===s)a.forEach(b,d);else if(c.isNumber(a.length))for(var e=
|
10
|
-
0,f=a.length;e<f;e++)b.call(d,a[e],e,a);else for(e in a)p.call(a,e)&&b.call(d,a[e],e,a)}catch(g){if(g!=r)throw g;}return a};c.map=function(a,b,d){if(t&&a.map===t)return a.map(b,d);var e=[];j(a,function(f,g,h){e[e.length]=b.call(d,f,g,h)});return e};c.reduce=c.foldl=c.inject=function(a,b,d,e){var f=d!==void 0;if(u&&a.reduce===u){if(e)b=c.bind(b,e);return f?a.reduce(b,d):a.reduce(b)}j(a,function(g,h,E){d=!f&&h===0?g:b.call(e,d,g,h,E)});return d};c.reduceRight=c.foldr=function(a,b,d,e){if(v&&a.reduceRight===
|
11
|
-
v){if(e)b=c.bind(b,e);return d!==void 0?a.reduceRight(b,d):a.reduceRight(b)}a=(c.isArray(a)?a.slice():c.toArray(a)).reverse();return c.reduce(a,b,d,e)};c.find=c.detect=function(a,b,d){var e;j(a,function(f,g,h){if(b.call(d,f,g,h)){e=f;c.breakLoop()}});return e};c.filter=c.select=function(a,b,d){if(w&&a.filter===w)return a.filter(b,d);var e=[];j(a,function(f,g,h){if(b.call(d,f,g,h))e[e.length]=f});return e};c.reject=function(a,b,d){var e=[];j(a,function(f,g,h){b.call(d,f,g,h)||(e[e.length]=f)});return e};
|
12
|
-
c.every=c.all=function(a,b,d){b=b||c.identity;if(x&&a.every===x)return a.every(b,d);var e=true;j(a,function(f,g,h){(e=e&&b.call(d,f,g,h))||c.breakLoop()});return e};c.some=c.any=function(a,b,d){b=b||c.identity;if(y&&a.some===y)return a.some(b,d);var e=false;j(a,function(f,g,h){if(e=b.call(d,f,g,h))c.breakLoop()});return e};c.include=c.contains=function(a,b){if(n&&a.indexOf===n)return a.indexOf(b)!=-1;var d=false;j(a,function(e){if(d=e===b)c.breakLoop()});return d};c.invoke=function(a,b){var d=i.call(arguments,
|
13
|
-
2);return c.map(a,function(e){return(b?e[b]:e).apply(e,d)})};c.pluck=function(a,b){return c.map(a,function(d){return d[b]})};c.max=function(a,b,d){if(!b&&c.isArray(a))return Math.max.apply(Math,a);var e={computed:-Infinity};j(a,function(f,g,h){g=b?b.call(d,f,g,h):f;g>=e.computed&&(e={value:f,computed:g})});return e.value};c.min=function(a,b,d){if(!b&&c.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};j(a,function(f,g,h){g=b?b.call(d,f,g,h):f;g<e.computed&&(e={value:f,computed:g})});
|
14
|
-
return e.value};c.sortBy=function(a,b,d){return c.pluck(c.map(a,function(e,f,g){return{value:e,criteria:b.call(d,e,f,g)}}).sort(function(e,f){var g=e.criteria,h=f.criteria;return g<h?-1:g>h?1:0}),"value")};c.sortedIndex=function(a,b,d){d=d||c.identity;for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(b)?e=g+1:f=g}return e};c.toArray=function(a){if(!a)return[];if(a.toArray)return a.toArray();if(c.isArray(a))return a;if(c.isArguments(a))return i.call(a);return c.values(a)};c.size=function(a){return c.toArray(a).length};
|
15
|
-
c.first=c.head=function(a,b,d){return b&&!d?i.call(a,0,b):a[0]};c.rest=c.tail=function(a,b,d){return i.call(a,c.isUndefined(b)||d?1:b)};c.last=function(a){return a[a.length-1]};c.compact=function(a){return c.filter(a,function(b){return!!b})};c.flatten=function(a){return c.reduce(a,function(b,d){if(c.isArray(d))return b.concat(c.flatten(d));b[b.length]=d;return b},[])};c.without=function(a){var b=i.call(arguments,1);return c.filter(a,function(d){return!c.include(b,d)})};c.uniq=c.unique=function(a,
|
16
|
-
b){return c.reduce(a,function(d,e,f){if(0==f||(b===true?c.last(d)!=e:!c.include(d,e)))d[d.length]=e;return d},[])};c.intersect=function(a){var b=i.call(arguments,1);return c.filter(c.uniq(a),function(d){return c.every(b,function(e){return c.indexOf(e,d)>=0})})};c.zip=function(){for(var a=i.call(arguments),b=c.max(c.pluck(a,"length")),d=Array(b),e=0;e<b;e++)d[e]=c.pluck(a,""+e);return d};c.indexOf=function(a,b){if(n&&a.indexOf===n)return a.indexOf(b);for(var d=0,e=a.length;d<e;d++)if(a[d]===b)return d;
|
17
|
-
return-1};c.lastIndexOf=function(a,b){if(z&&a.lastIndexOf===z)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};c.range=function(a,b,d){var e=i.call(arguments),f=e.length<=1;a=f?0:e[0];b=f?e[0]:e[1];d=e[2]||1;e=Math.max(Math.ceil((b-a)/d),0);f=0;for(var g=Array(e);f<e;){g[f++]=a;a+=d}return g};c.bind=function(a,b){var d=i.call(arguments,2);return function(){return a.apply(b||{},d.concat(i.call(arguments)))}};c.bindAll=function(a){var b=i.call(arguments,1);if(b.length==
|
18
|
-
0)b=c.functions(a);j(b,function(d){a[d]=c.bind(a[d],a)});return a};c.memoize=function(a,b){var d={};b=b||c.identity;return function(){var e=b.apply(this,arguments);return e in d?d[e]:d[e]=a.apply(this,arguments)}};c.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};c.defer=function(a){return c.delay.apply(c,[a,1].concat(i.call(arguments,1)))};c.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments));return b.apply(b,d)}};c.compose=
|
19
|
-
function(){var a=i.call(arguments);return function(){for(var b=i.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};c.keys=D||function(a){if(c.isArray(a))return c.range(0,a.length);var b=[],d;for(d in a)if(p.call(a,d))b[b.length]=d;return b};c.values=function(a){return c.map(a,c.identity)};c.functions=c.methods=function(a){return c.filter(c.keys(a),function(b){return c.isFunction(a[b])}).sort()};c.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});
|
20
|
-
return a};c.clone=function(a){return c.isArray(a)?a.slice():c.extend({},a)};c.tap=function(a,b){b(a);return a};c.isEqual=function(a,b){if(a===b)return true;var d=typeof a;if(d!=typeof b)return false;if(a==b)return true;if(!a&&b||a&&!b)return false;if(a.isEqual)return a.isEqual(b);if(c.isDate(a)&&c.isDate(b))return a.getTime()===b.getTime();if(c.isNaN(a)&&c.isNaN(b))return false;if(c.isRegExp(a)&&c.isRegExp(b))return a.source===b.source&&a.global===b.global&&a.ignoreCase===b.ignoreCase&&a.multiline===
|
21
|
-
b.multiline;if(d!=="object")return false;if(a.length&&a.length!==b.length)return false;d=c.keys(a);var e=c.keys(b);if(d.length!=e.length)return false;for(var f in a)if(!(f in b)||!c.isEqual(a[f],b[f]))return false;return true};c.isEmpty=function(a){if(c.isArray(a)||c.isString(a))return a.length===0;for(var b in a)if(p.call(a,b))return false;return true};c.isElement=function(a){return!!(a&&a.nodeType==1)};c.isArray=m||function(a){return!!(a&&a.concat&&a.unshift&&!a.callee)};c.isArguments=function(a){return!!(a&&
|
22
|
-
a.callee)};c.isFunction=function(a){return!!(a&&a.constructor&&a.call&&a.apply)};c.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};c.isNumber=function(a){return a===+a||C.call(a)==="[object Number]"};c.isBoolean=function(a){return a===true||a===false};c.isDate=function(a){return!!(a&&a.getTimezoneOffset&&a.setUTCFullYear)};c.isRegExp=function(a){return!!(a&&a.test&&a.exec&&(a.ignoreCase||a.ignoreCase===false))};c.isNaN=function(a){return c.isNumber(a)&&isNaN(a)};c.isNull=function(a){return a===
|
23
|
-
null};c.isUndefined=function(a){return typeof a=="undefined"};c.noConflict=function(){o._=A;return this};c.identity=function(a){return a};c.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};c.breakLoop=function(){throw r;};c.mixin=function(a){j(c.functions(a),function(b){F(b,c[b]=a[b])})};var G=0;c.uniqueId=function(a){var b=G++;return a?a+b:b};c.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g};c.template=function(a,b){var d=c.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+
|
24
|
-
a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(e,f){return"',"+f.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||null,function(e,f){return"');"+f.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return b?d(b):d};var l=function(a){this._wrapped=a};c.prototype=l.prototype;var q=function(a,b){return b?c(a).chain():a},F=function(a,b){l.prototype[a]=function(){var d=
|
25
|
-
i.call(arguments);B.call(d,this._wrapped);return q(b.apply(c,d),this._chain)}};c.mixin(c);j(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=k[a];l.prototype[a]=function(){b.apply(this._wrapped,arguments);return q(this._wrapped,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];l.prototype[a]=function(){return q(b.apply(this._wrapped,arguments),this._chain)}});l.prototype.chain=function(){this._chain=true;return this};l.prototype.value=function(){return this._wrapped}}
|
data/lib/qme/ext/record.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
# Extensions to the Record model in health-data-standards to support
|
2
|
-
# quality measure calculation
|
3
|
-
class Record
|
4
|
-
|
5
|
-
def procedures_performed
|
6
|
-
@procedures_performed = procedures.to_a + immunizations.to_a + medications.to_a
|
7
|
-
end
|
8
|
-
|
9
|
-
def procedure_results
|
10
|
-
@procedure_results ||= results.to_a + vital_signs.to_a + procedures.to_a
|
11
|
-
end
|
12
|
-
|
13
|
-
def laboratory_tests
|
14
|
-
@laboratory_tests ||= results.to_a + vital_signs.to_a
|
15
|
-
end
|
16
|
-
|
17
|
-
def all_meds
|
18
|
-
@all_meds ||= medications.to_a + immunizations.to_a
|
19
|
-
end
|
20
|
-
|
21
|
-
def active_diagnosis
|
22
|
-
@active_diagnosis ||= conditions.any_of({:status => 'active'}, {:status => nil}, {:ordinality => 'principal'}).to_a +
|
23
|
-
social_history.any_of({:status => 'active'}, {:status => nil}).to_a
|
24
|
-
end
|
25
|
-
|
26
|
-
def inactive_diagnosis
|
27
|
-
@inactive_diagnosis ||= conditions.any_of({:status => 'inactive'}).to_a +
|
28
|
-
social_history.any_of({:status => 'inactive'}).to_a
|
29
|
-
end
|
30
|
-
|
31
|
-
def resolved_diagnosis
|
32
|
-
@resolved_diagnosis ||= conditions.any_of({:status => 'resolved'}).to_a +
|
33
|
-
social_history.any_of({:status => 'resolved'}).to_a
|
34
|
-
end
|
35
|
-
|
36
|
-
def all_problems
|
37
|
-
@all_problems ||= conditions.to_a + social_history.to_a
|
38
|
-
end
|
39
|
-
|
40
|
-
def all_devices
|
41
|
-
@all_devices ||= conditions.to_a + procedures.to_a + care_goals.to_a + medical_equipment.to_a
|
42
|
-
end
|
43
|
-
end
|
data/lib/qme/importer/entry.rb
DELETED
@@ -1,126 +0,0 @@
|
|
1
|
-
module QME
|
2
|
-
module Importer
|
3
|
-
# Object that represents a CDA Entry (or act, observation, etc.)
|
4
|
-
class Entry
|
5
|
-
attr_accessor :start_time, :end_time, :time, :status, :ordinality, :description
|
6
|
-
attr_reader :codes, :value
|
7
|
-
|
8
|
-
def initialize
|
9
|
-
@codes = {}
|
10
|
-
@value = {}
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.from_event_hash(event)
|
14
|
-
entry = Entry.new
|
15
|
-
if event['code']
|
16
|
-
entry.add_code(event['code'], event['code_set'])
|
17
|
-
elsif event['codes']
|
18
|
-
entry.instance_eval { @codes = event['codes'] }
|
19
|
-
end
|
20
|
-
|
21
|
-
entry.time = event['time']
|
22
|
-
if event['value']
|
23
|
-
entry.set_value(event['value'], event['unit'])
|
24
|
-
end
|
25
|
-
if event['description']
|
26
|
-
entry.description = event['description']
|
27
|
-
end
|
28
|
-
if event['status']
|
29
|
-
entry.status = event['status']
|
30
|
-
end
|
31
|
-
if event['ordinality']
|
32
|
-
entry.ordinality = event['ordinality']
|
33
|
-
end
|
34
|
-
entry
|
35
|
-
end
|
36
|
-
|
37
|
-
# Add a code into the Entry
|
38
|
-
# @param [String] code the code to add
|
39
|
-
# @param [String] code_system the code system that the code belongs to
|
40
|
-
def add_code(code, code_system)
|
41
|
-
@codes[code_system] ||= []
|
42
|
-
@codes[code_system] << code
|
43
|
-
end
|
44
|
-
|
45
|
-
# Sets the value for the entry
|
46
|
-
# @param [String] scalar the value
|
47
|
-
# @param [String] units the units of the scalar value
|
48
|
-
def set_value(scalar, units=nil)
|
49
|
-
@value[:scalar] = scalar
|
50
|
-
@value[:units] = units
|
51
|
-
end
|
52
|
-
|
53
|
-
# Checks if a code is in the list of possible codes
|
54
|
-
# @param [Array] code_set an Array of Hashes that describe the values for code sets
|
55
|
-
# @return [true, false] whether the code is in the list of desired codes
|
56
|
-
def is_in_code_set?(code_set)
|
57
|
-
@codes.keys.each do |code_system|
|
58
|
-
all_codes_in_system = code_set.find_all {|set| set['set'] == code_system}
|
59
|
-
all_codes_in_system.each do |codes_in_system|
|
60
|
-
matching_codes = codes_in_system['values'] & @codes[code_system]
|
61
|
-
if matching_codes.length > 0
|
62
|
-
return true
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
false
|
67
|
-
end
|
68
|
-
|
69
|
-
# Tries to find a single point in time for this entry. Will first return time if it is present,
|
70
|
-
# then fall back to start_time and finally end_time
|
71
|
-
def as_point_in_time
|
72
|
-
if @time
|
73
|
-
@time
|
74
|
-
elsif @start_time
|
75
|
-
@start_time
|
76
|
-
else
|
77
|
-
@end_time
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# Checks to see if this Entry can be used as a date range
|
82
|
-
# @return [true, false] If the Entry has a start and end time returns true, false otherwise.
|
83
|
-
def is_date_range?
|
84
|
-
(! @start_time.nil?) && (! @end_time.nil?)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Checks to see if this Entry is usable for measure calculation. This means that it contains
|
88
|
-
# at least one code and has one of its time properties set (start, end or time)
|
89
|
-
# @return [true, false]
|
90
|
-
def usable?
|
91
|
-
(! @codes.empty?) && ((! @start_time.nil?) || (! @end_time.nil?) || (! @time.nil?))
|
92
|
-
end
|
93
|
-
|
94
|
-
# Creates a Hash for this Entry
|
95
|
-
# @return [Hash] a Hash representing the Entry
|
96
|
-
def to_hash
|
97
|
-
entry_hash = {'_id' => BSON::ObjectId.new}
|
98
|
-
entry_hash['codes'] = @codes
|
99
|
-
unless @value.empty?
|
100
|
-
entry_hash['value'] = @value
|
101
|
-
end
|
102
|
-
|
103
|
-
if is_date_range?
|
104
|
-
entry_hash['start_time'] = @start_time
|
105
|
-
entry_hash['end_time'] = @end_time
|
106
|
-
else
|
107
|
-
entry_hash['time'] = as_point_in_time
|
108
|
-
end
|
109
|
-
|
110
|
-
if @status
|
111
|
-
entry_hash['status'] = @status
|
112
|
-
end
|
113
|
-
|
114
|
-
if @ordinality
|
115
|
-
entry_hash['ordinality'] = @ordinality
|
116
|
-
end
|
117
|
-
|
118
|
-
if @description
|
119
|
-
entry_hash['description'] = @description
|
120
|
-
end
|
121
|
-
|
122
|
-
entry_hash
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
@@ -1,117 +0,0 @@
|
|
1
|
-
module QME
|
2
|
-
module Importer
|
3
|
-
# Class that can be used to create a HITSP C32 importer for any quality measure. This class will construct
|
4
|
-
# several SectionImporter for the various sections of the C32. When initialized with a JSON measure definition
|
5
|
-
# it can then be passed a C32 document and will return a Hash with all of the information needed to calculate the measure.
|
6
|
-
class GenericImporter
|
7
|
-
|
8
|
-
class << self
|
9
|
-
attr_accessor :warnings
|
10
|
-
end
|
11
|
-
|
12
|
-
@warnings = []
|
13
|
-
|
14
|
-
# Creates a generic importer for any quality measure.
|
15
|
-
#
|
16
|
-
# @param [Hash] definition A measure definition described in JSON
|
17
|
-
def initialize(definition)
|
18
|
-
@definition = definition
|
19
|
-
@warnings = []
|
20
|
-
end
|
21
|
-
|
22
|
-
# Parses a HITSP C32 document and returns a Hash of information related to the measure
|
23
|
-
#
|
24
|
-
# @param [Hash] patient_hash representation of a patient
|
25
|
-
# @return [Hash] measure information
|
26
|
-
def parse(patient)
|
27
|
-
measure_info = {}
|
28
|
-
@definition['measure'].each_pair do |property, description|
|
29
|
-
raise "No standard_category for #{property}" if !description['standard_category']
|
30
|
-
matcher = PropertyMatcher.new(description)
|
31
|
-
entry_list = filter_for_property(description['standard_category'], description['qds_data_type'], patient)
|
32
|
-
if ! entry_list.empty?
|
33
|
-
matched_list = matcher.match(entry_list)
|
34
|
-
measure_info[property] = matched_list if matched_list.length > 0
|
35
|
-
end
|
36
|
-
end
|
37
|
-
measure_info
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def filter_for_property(standard_category, qds_data_type, patient)
|
43
|
-
# Currently unsupported categories: negation_rationale, risk_category_assessment
|
44
|
-
case standard_category
|
45
|
-
when 'encounter'
|
46
|
-
patient.encounters
|
47
|
-
when 'immunization'
|
48
|
-
patient.immunizations
|
49
|
-
when 'procedure'
|
50
|
-
case qds_data_type
|
51
|
-
when 'procedure_performed'
|
52
|
-
patient.procedures_performed
|
53
|
-
when 'procedure_adverse_event', 'procedure_intolerance'
|
54
|
-
patient.allergies
|
55
|
-
when 'procedure_result'
|
56
|
-
patient.procedure_results
|
57
|
-
end
|
58
|
-
when 'risk_category_assessment'
|
59
|
-
patient.procedures
|
60
|
-
when 'communication'
|
61
|
-
patient.procedures
|
62
|
-
when 'laboratory_test'
|
63
|
-
patient.laboratory_tests
|
64
|
-
when 'physical_exam'
|
65
|
-
patient.procedure_results
|
66
|
-
when 'medication'
|
67
|
-
case qds_data_type
|
68
|
-
when 'medication_dispensed', 'medication_order', 'medication_active', 'medication_administered'
|
69
|
-
patient.all_meds
|
70
|
-
when 'medication_allergy', 'medication_intolerance', 'medication_adverse_event'
|
71
|
-
patient.allergies
|
72
|
-
end
|
73
|
-
when 'diagnosis_condition_problem'
|
74
|
-
case qds_data_type
|
75
|
-
when 'diagnosis_active'
|
76
|
-
patient.active_diagnosis
|
77
|
-
when 'diagnosis_active_priority_principal'
|
78
|
-
patient.active_diagnosis
|
79
|
-
when 'diagnosis_inactive'
|
80
|
-
patient.inactive_diagnosis
|
81
|
-
when 'diagnosis_resolved'
|
82
|
-
patient.resolved_diagnosis
|
83
|
-
end
|
84
|
-
when 'symptom'
|
85
|
-
patient.all_problems
|
86
|
-
when 'individual_characteristic'
|
87
|
-
patient.all_problems
|
88
|
-
when 'device'
|
89
|
-
case qds_data_type
|
90
|
-
when 'device_applied'
|
91
|
-
patient.all_devices
|
92
|
-
when 'device_allergy'
|
93
|
-
patient.allergies
|
94
|
-
end
|
95
|
-
when 'care_goal'
|
96
|
-
patient.care_goals
|
97
|
-
when 'diagnostic_study'
|
98
|
-
case qds_data_type
|
99
|
-
|
100
|
-
when 'diagnostic_study_performed'
|
101
|
-
patient.procedures
|
102
|
-
when 'diagnostic_study_result'
|
103
|
-
patient.procedure_results
|
104
|
-
end
|
105
|
-
when 'substance'
|
106
|
-
patient.allergies
|
107
|
-
else
|
108
|
-
unless self.class.warnings.include?(standard_category)
|
109
|
-
puts "Warning: Unsupported standard_category (#{standard_category})"
|
110
|
-
self.class.warnings << standard_category
|
111
|
-
end
|
112
|
-
[]
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
module QME
|
2
|
-
module Importer
|
3
|
-
class MeasurePropertiesGenerator
|
4
|
-
include Singleton
|
5
|
-
|
6
|
-
def initialize
|
7
|
-
@measure_importers = {}
|
8
|
-
end
|
9
|
-
|
10
|
-
# Adds a GenericImporter that will be used to extract
|
11
|
-
# information about a measure from a patient Record.
|
12
|
-
def add_measure(measure_id, importer)
|
13
|
-
@measure_importers[measure_id] = importer
|
14
|
-
end
|
15
|
-
|
16
|
-
# Generates denormalized measure information
|
17
|
-
# Measure information is contained in a Hash that hash
|
18
|
-
# the measure id as a key, and the denormalized
|
19
|
-
# measure information as a value
|
20
|
-
#
|
21
|
-
# @param [Record] patient - populated patient record
|
22
|
-
# @returns Hash with denormalized measure information
|
23
|
-
def generate_properties(patient)
|
24
|
-
measures = {}
|
25
|
-
@measure_importers.each_pair do |measure_id, importer|
|
26
|
-
measures[measure_id] = importer.parse(patient)
|
27
|
-
end
|
28
|
-
measures
|
29
|
-
end
|
30
|
-
|
31
|
-
# The same as generate_properties but addes the denormalized
|
32
|
-
# measure information into the Record and saves it.
|
33
|
-
def generate_properties!(patient)
|
34
|
-
patient['measures'] = generate_properties(patient)
|
35
|
-
patient.save!
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
@@ -1,110 +0,0 @@
|
|
1
|
-
module QME
|
2
|
-
module Importer
|
3
|
-
# Compares Entry objects to measure definition properties.
|
4
|
-
class PropertyMatcher
|
5
|
-
|
6
|
-
def initialize(property_description)
|
7
|
-
@property_description = property_description
|
8
|
-
end
|
9
|
-
|
10
|
-
# Looks through an Array of Entry objects to see if any of them match the codes needed
|
11
|
-
# for a measure property. Will return different types of Arrays depending on the schema
|
12
|
-
# of the property
|
13
|
-
# @param [Array] entry_list an Array of Entry objects
|
14
|
-
# @return An Array of goodness that is ready to be inserted into a measure property on a patient record
|
15
|
-
def match(entry_list)
|
16
|
-
if is_date_list_property?
|
17
|
-
extract_date_list(entry_list)
|
18
|
-
elsif is_value_date_property?
|
19
|
-
extract_value_date_list(entry_list)
|
20
|
-
elsif is_date_range_property?
|
21
|
-
extract_date_range_list(entry_list)
|
22
|
-
else
|
23
|
-
raise "Unknown property schema for property #{@property_description['description']}"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# Extracts the dates of any CDA entries that meet the code set defined for measure property.
|
28
|
-
#
|
29
|
-
# @param [Array] entry_list an Array of Entry objects
|
30
|
-
# @return [Array] Provides an Array of dates for entries that have codes inside of the measure code set
|
31
|
-
# Dates will be represented as an Integer in seconds since the epoch
|
32
|
-
def extract_date_list(entry_list)
|
33
|
-
basic_extractor(entry_list) do |entry, matching_values|
|
34
|
-
matching_values << entry.as_point_in_time
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
# Extracts the dates of any CDA entries that meet the code set defined for measure property.
|
39
|
-
#
|
40
|
-
# @param [Array] entry_list an Array of Entry objects
|
41
|
-
# @return [Array] Provides an Array of Hashes for entries that have codes inside of the measure code set
|
42
|
-
# Hashes will have a "value" and "date" property containing the respective data
|
43
|
-
def extract_value_date_list(entry_list)
|
44
|
-
basic_extractor(entry_list) do |entry, matching_values|
|
45
|
-
value = entry.value[:scalar]
|
46
|
-
if value
|
47
|
-
if @property_description['items']['properties']['value']['type'] == 'number'
|
48
|
-
value = value.to_f
|
49
|
-
elsif @property_description['items']['properties']['value']['type'] == 'boolean'
|
50
|
-
value = value.to_boolean
|
51
|
-
end
|
52
|
-
|
53
|
-
matching_values << {'date' => entry.as_point_in_time, 'value' => value}
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# Extracts the dates of any CDA entries that meet the code set defined for measure property.
|
59
|
-
#
|
60
|
-
# @param [Array] entry_list an Array of Entry objects
|
61
|
-
# @return [Array] Provides an Array of Hashes for entries that have codes inside of the measure code set
|
62
|
-
# Hashes will have a "start" and "end" property containing the respective data
|
63
|
-
def extract_date_range_list(entry_list)
|
64
|
-
basic_extractor(entry_list) do |entry, matching_values|
|
65
|
-
if entry.is_date_range?
|
66
|
-
matching_values << {'start' => entry.start_time, 'end' => entry.end_time}
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
# Determines if the property is a list of dates
|
72
|
-
# @return [Boolean] true of false depending on the property
|
73
|
-
def is_date_list_property?
|
74
|
-
@property_description['type'] == 'array' && @property_description['items']['type'] == 'number'
|
75
|
-
end
|
76
|
-
|
77
|
-
# Determines if the property is a list of date and value hashes
|
78
|
-
# @return [Boolean] true of false depending on the property
|
79
|
-
def is_value_date_property?
|
80
|
-
@property_description['type'] == 'array' && @property_description['items']['type'] == 'object' &&
|
81
|
-
@property_description['items']['properties']['value'] &&
|
82
|
-
@property_description['items']['properties']['date']
|
83
|
-
end
|
84
|
-
|
85
|
-
# Determines if the property is a list of date ranges represented by a Hash with start and end
|
86
|
-
# keys
|
87
|
-
# @return [Boolean] true of false depending on the property
|
88
|
-
def is_date_range_property?
|
89
|
-
@property_description['type'] == 'array' && @property_description['items']['type'] == 'object' &&
|
90
|
-
@property_description['items']['properties']['start'] &&
|
91
|
-
@property_description['items']['properties']['end']
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def basic_extractor(entry_list)
|
97
|
-
matching_values = []
|
98
|
-
entry_list.each do |entry|
|
99
|
-
if entry.usable?
|
100
|
-
if entry.is_in_code_set?(@property_description['codes'])
|
101
|
-
yield entry, matching_values
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
matching_values
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|