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.
- 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
|