├── .gitignore ├── LICENSE ├── README.md ├── cloudfoundry.json ├── config ├── config.js └── index.js ├── controllers ├── ccda_scorecard.js ├── ccda_scorecard_xquery.js └── examples.js ├── launch.js ├── lib ├── BaseRubric.js ├── common.js ├── grader.js └── rubrics.js ├── npm-shrinkwrap.json ├── package.json ├── public ├── angular-ui │ └── ui-bootstrap-custom-0.4.0.js ├── angular │ ├── angular-resource.js │ ├── angular-sanitize.js │ └── angular.js ├── bootstrap │ ├── css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap-responsive.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── jquery.js ├── ccdaScorecard │ ├── controllers.js │ ├── example.ccda.xml │ ├── histo.html │ ├── index.html │ ├── modules.js │ └── templates │ │ ├── authorize-app.html │ │ ├── index.html │ │ ├── patient-search.html │ │ ├── patient-selected.html │ │ └── select-patient.html ├── images │ └── smart-logo.png ├── js │ ├── Markdown.Converter.js │ ├── d3.v2.min.js │ ├── modernizr.js │ └── scrollTo.js ├── postit.html └── stylesheets │ └── style.css ├── routes.js ├── rubrics ├── codes-01-exist.js ├── codes-01-exist.json ├── codes-01-exist.report.ejs ├── codes-02-displaynames-correct.js ├── codes-02-displaynames-correct.json ├── codes-02-displaynames-correct.report.ejs ├── dates-01-precision.js ├── dates-01-precision.json ├── disabled │ ├── nulliness.js │ └── nulliness.json ├── labcodes.js ├── labcodes.json ├── labcodes.report.ejs ├── medcodes.js ├── medcodes.json ├── medcodes.report.ejs ├── partials │ ├── codeTable.ejs │ └── mistakes.ejs ├── problemcodes.js ├── problemcodes.json ├── problemcodes.report.ejs ├── problemstatus.js ├── problemstatus.json ├── shared │ ├── codes.js │ ├── templates.json │ ├── ucum-essence.xml │ ├── ucum.extract.peg │ ├── ucum.peg │ └── ucum_parser.js ├── smoking-03-structured-smoking-status.js ├── smoking-03-structured-smoking-status.json ├── smokingcodes.js ├── smokingcodes.json ├── smokingcodes.report.ejs ├── smokingstructure.js ├── smokingstructure.json ├── templateids.js ├── templateids.json ├── templateids.report.ejs ├── units-01-valid-ucum.js ├── units-01-valid-ucum.json ├── vitals-01-structured.js ├── vitals-01-structured.json ├── vitals-02-loinc.js ├── vitals-02-loinc.json ├── vitals-02-loinc.report.ejs ├── vitals-03-ucum.js ├── vitals-03-ucum.json └── vitals-03-ucum.report.ejs └── value_sets ├── README.md ├── assign_value_sets.py ├── extract_concepts_from_mysql.py ├── import_phinda_vocab.pyc ├── import_phinvads_vocab.py ├── mustaine ├── __init__.py ├── _util.py ├── client.py ├── encoder.py ├── parser.py └── protocol.py ├── other_valuesets.txt ├── phinvads_valueset_oids.json └── rxnorm ├── genlinks.py └── import_sqlite /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | *.tmproj 4 | *~ 5 | .project 6 | *# 7 | .#* 8 | *.swo 9 | *.swp 10 | node_modules 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Boston Children's Hospital 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this software except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C-CDA Scorecard 2 | 3 | ## Demo online 4 | [http://ccda-scorecard.smartplatforms.org](http://ccda-scorecard.smartplatforms.org) 5 | 6 | ## Promoting best practices in C-CDA generation 7 | 8 | Achieving interoperability with Consolidated CDA documents means agreeing 9 | on "best practices" above and beyond the base specification. This tool captures 10 | best practices as `rubrics` in code, so you can automatically assess whether 11 | your documents conform. For example... 12 | 13 | ### Example Best Practice: "Problem List entries should have one consistent status." 14 | 15 | The C-CDA spec provides three distinct places to record a problem status. The 16 | best practice recommendation is to make sure they all agree! See below for an 17 | example of a rubric to help automate testing for this best practice. 18 | 19 | ## Using and Extending 20 | 21 | ### Setup 22 | * nodejs 23 | * mongodb 24 | * libxml2 25 | 26 | ``` 27 | $ git clone https://github.com/chb/ccdaScorecard 28 | $ cd ccdaScorecard 29 | $ npm update 30 | ``` 31 | 32 | ### Run 33 | `node launch.js` 34 | 35 | ### Write new and better rubrics 36 | Check out our initial examples in [rubrics](ccdaScorecard/tree/master/rubrics) 37 | 38 | ### Build a better client 39 | This repo includes a simple example Web UI built agains the CCDA Scorecard REST API. 40 | It's implemented as a static HTML5 / JavaScript Web app at: 41 | [public/ccdaScorecard/index.html](ccdaScorecard/tree/master/public/ccdaScorecard/index.html) 42 | 43 | This can serve as a launch point for bigger and better. 44 | 45 | 46 | ## The pieces and the REST API 47 | 48 | ### Rubrics 49 | `GET /v1/ccda-scorecard/rubrics(/:rubricId)` 50 | 51 | Best practices are encoded as scoring `rubrics` that include a description, 52 | scoring table, and some procedural JavaScript code that checks a C-CDA for 53 | conformance to the rubric. Some rubrics feature binary grading (1 point = 54 | pass, 0 points = fail), while others provide point-based grading (say 0-3 55 | points). So conformance to a rubric may not be all-or-nothing. For example 56 | there's a rubric to check whether problems have SNOMED codes. A problem list 57 | containing *mostly* SNOMED codes is **much better** than a problem list with 58 | *none*, and rubrics can take this into account with point-based grading. 59 | 60 | Here's what the JSON description of a rubric might look like: 61 | 62 | ```js 63 | { 64 | "id": "labcodes", 65 | "scorecards": ["c-cda", "smart"], 66 | "category": ["Lab Results", "Codes"], 67 | "description": "Lab Results coded with LOINC's top 2K codes", 68 | "detail": "Lab results should be coded using LOINC. In pratice LOINC is huge, but 2000 codes cover 98% of real-world usage. This means that most results should be covered by the 2000+ most common LOINC codes published by Regenstrief.", 69 | "maxPoints": 3, 70 | "points": { 71 | "3": "> 80% of lab results have a top-2K LOINC code", 72 | "2": "> 50% of lab results have a top-2K LOINC code", 73 | "1": "At least one lab result has a top-2K LOINC code", 74 | "0": "No lab results have a top-2K LOINC code" 75 | }, 76 | "doesNotApply": "No lab results in document" 77 | } 78 | ``` 79 | 80 | The URL above fetches a JSON description of all rubrics (or a single one). 81 | 82 | ### Scorecard 83 | `POST /v1/ccda-scorecard/request` 84 | 85 | Submit a C-CDA document in the body of a scorecard request to obtain a 86 | scorecard in JSON. The scorecard will include a grade for each rubric 87 | on the CCDA Scorecard app. 88 | 89 | ### Stats 90 | 91 | As documents are validated, they're logged and used to calculate statistics 92 | about how often best practices are followed. Each rubric is associated with 93 | summary data, so you can easily get a count of how often each grade is achieved 94 | -- and get a better sense of how conformant your document is relative to the 95 | broader community. 96 | 97 | For example, a rubric for a well-followed best practice might look like: 98 | 99 | ``` 100 | { 101 | "id": "vitals-using-loinc", 102 | "counts": { 103 | "3": 25, 104 | "2": 8, 105 | "1": 2, 106 | "0": 1, 107 | "N/A": 4 108 | } 109 | } 110 | ``` 111 | 112 | The URL above fetches a JSON description of all stats (or stats for a single rubric). 113 | -------------------------------------------------------------------------------- /cloudfoundry.json: -------------------------------------------------------------------------------- 1 | { 2 | "cfAutoconfig": false 3 | } 4 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | var mongodb = require('mongodb'); 2 | var Db = require('mongodb').Db; 3 | var Server = require('mongodb').Server; 4 | var events = require('events'); 5 | var async = require('async'); 6 | 7 | var port = (process.env.VMC_APP_PORT || 3000); 8 | var host = (process.env.VCAP_APP_HOST || 'localhost'); 9 | 10 | var mongoVocab = { 11 | "hostname":"localhost", 12 | "port":27017, 13 | "username":"", 14 | "password":"", 15 | "name":"", 16 | "db":"vocab" 17 | } 18 | 19 | var mongoCcdaScorecard = { 20 | "hostname":"localhost", 21 | "port":27017, 22 | "username":"", 23 | "password":"", 24 | "name":"", 25 | "db":"ccdaScorecard" 26 | } 27 | 28 | var generate_mongo_url = function(obj){ 29 | obj.hostname = (obj.hostname || 'localhost'); 30 | obj.port = (obj.port || 27017); 31 | obj.db = (obj.db || 'test'); 32 | if(obj.username && obj.password){ 33 | return "mongodb://" + obj.username + ":" + obj.password + "@" + obj.hostname + ":" + obj.port + "/" + obj.db + "?safe=true"; 34 | } 35 | else{ 36 | return "mongodb://" + obj.hostname + ":" + obj.port + "/" + obj.db + "?safe=true"; 37 | } 38 | } 39 | var mongoVocabUrl = process.env.MONGOLAB_VOCAB_URI || generate_mongo_url(mongoVocab); 40 | var mongoCcdaScorecardUrl = process.env.MONGOLAB_CCDA_SCORECARD_URI || generate_mongo_url(mongoCcdaScorecard); 41 | 42 | var dbstate = new events.EventEmitter(); 43 | 44 | function connectToDb(dburl, exportname){ 45 | return function(cb){ 46 | mongodb.connect(dburl, { 47 | db: {native_parser: false}, 48 | server: {auto_reconnect: true} 49 | }, function(err, conn){ 50 | module.exports.db[exportname] = conn; 51 | cb(err); 52 | }); 53 | 54 | }; 55 | }; 56 | 57 | async.parallel([ 58 | connectToDb(mongoVocabUrl, "vocab"), 59 | connectToDb(mongoCcdaScorecardUrl, "ccdaScorecard"), 60 | ], 61 | function(err){ 62 | if (err){ 63 | console.log("Db connect err", err); 64 | } 65 | var db = module.exports.db; 66 | dbstate.emit("ready"); 67 | dbstate.on = function(x, f){if (x==="ready") f();}; 68 | }); 69 | 70 | module.exports = { 71 | env: process.env.NODE_ENV || 'development', 72 | port: port, 73 | host: host, 74 | publicUri: process.env.PUBLIC_URI || "http://localhost:3000", 75 | appServer: process.env.APP_SERVER || "http://localhost:3001/apps", 76 | db: {}, 77 | dbstate: dbstate, 78 | shutdown: function(){ 79 | Object.keys(module.exports.db).forEach(function(k){ 80 | module.exports.db[k].close(); 81 | }); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | , winston = require('winston') 3 | , poweredBy = require('connect-powered-by') 4 | , passport = require('passport') 5 | , https = require('https') 6 | , fs = require('fs') 7 | , config = require('./config'); 8 | 9 | module.exports = config; 10 | config.launch = launch; 11 | 12 | function launch() { 13 | var app = config.app = express(); 14 | app.set('views', __dirname + '../views'); 15 | app.set('view engine', 'ejs'); 16 | app.engine('ejs', require('ejs').__express); 17 | 18 | app.use(express.logger()); 19 | app.use(express.favicon()); 20 | 21 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 22 | 23 | app.enable('trust proxy'); 24 | 25 | app.use (function(req, res, next) { 26 | winston.info("protocol: " + req.protocol); 27 | if (!req.secure && config.publicUri.match(/^https/)) { 28 | winston.info("redirecting to secure: " + req.originalUrl); 29 | return res.redirect(config.publicUri + req.originalUrl); 30 | } 31 | next(); 32 | }); 33 | 34 | var myBodyParser = express.bodyParser(); 35 | app.use (function(req, res, next) { 36 | 37 | var complete = { mine: false, theirs: false }; 38 | 39 | function checkDone(){ 40 | if (complete.mine && complete.theirs){ 41 | next(); 42 | } 43 | }; 44 | 45 | req.rawBody = ''; 46 | req.setEncoding('utf8'); 47 | 48 | myBodyParser(req, res, function(){ 49 | complete.theirs = true; 50 | checkDone(); 51 | }); 52 | 53 | req.on('data', function(chunk) { 54 | req.rawBody += chunk; 55 | console.log("got some data", chunk.length, req.rawBody.length); 56 | }); 57 | 58 | req.on('end', function(){ 59 | complete.mine = true; 60 | checkDone(); 61 | }) 62 | }); 63 | 64 | app.use(express.bodyParser()); 65 | 66 | app.use(function(req, res, next){ 67 | res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS'); 68 | res.setHeader("Access-Control-Allow-Origin", "*"); 69 | next(); 70 | }); 71 | 72 | app.use('/static', express.static(__dirname + '/../public')); 73 | app.use(express.cookieParser('express-cookie-secret-here')); 74 | app.use(express.session()); 75 | app.use(passport.initialize()); 76 | app.use(passport.session()); 77 | app.use(app.router); 78 | 79 | require('../routes'); 80 | 81 | app.listen(config.port); 82 | winston.info("launched server on port " + config.port); 83 | }; 84 | -------------------------------------------------------------------------------- /controllers/ccda_scorecard.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | var hashish = require('hashish'); 3 | var async = require('async'); 4 | var fs = require('fs'); 5 | var grade = require('../lib/grader'); 6 | var rubrics = require('../lib/rubrics'); 7 | var db = require('../config').db; 8 | 9 | var Controller = module.exports = {}; 10 | 11 | var indexSource = fs.readFileSync(__dirname+"/../public/ccdaScorecard/index.html").toString(); 12 | 13 | var allRubrics = {}; 14 | Object.keys(rubrics).forEach(function(k){ 15 | allRubrics[k] = rubrics[k].json; 16 | }); 17 | 18 | 19 | Controller.postToUi = function(req, res, next){ 20 | console.log(req.body); 21 | res.end(indexSource.replace("var defaultCcda = null;", "var defaultCcda = \""+ 22 | encodeURIComponent(req.body.ccda) + "\";")); 23 | } 24 | 25 | Controller.gradeRequest = function(req, res, next) { 26 | winston.info('grading a CCD request of length ' + req.rawBody.length); 27 | winston.info('grading a CCD request of length ' + JSON.stringify(req.headers)); 28 | grade({ 29 | src: req.rawBody, 30 | save: (req.query.example !== "true") 31 | }, function(err, report){ 32 | res.json(report); 33 | }); 34 | }; 35 | 36 | Controller.rubricOne = function(req, res, next) { 37 | winston.info('getting one rubric: ' + req.params); 38 | res.json(allRubrics[req.params.rid]); 39 | }; 40 | 41 | Controller.rubricAll = function(req, res, next) { 42 | winston.info('getting all rubrics'); 43 | res.json(allRubrics); 44 | }; 45 | 46 | 47 | function allStats(done){ 48 | db.ccdaScorecard.collection("scoreStats", function(err, scoreStats){ 49 | 50 | console.log("Fond scoreStats collection", err, !!scoreStats); 51 | if (err){ 52 | return done(err); 53 | } 54 | 55 | scoreStats.find({}).toArray(function(err, allstats){ 56 | if (err) { 57 | console.log("failed to find stats", err, allstats); 58 | return done(err); 59 | } 60 | console.log("found stats", allstats); 61 | var ret = {}; 62 | allstats.forEach(function(s){ 63 | s.id = s._id; 64 | delete s._id; 65 | ret[s.id] = s; 66 | }); 67 | done(err, ret); 68 | }); 69 | 70 | }); 71 | }; 72 | 73 | Controller.statsAll = function(req, res, next) { 74 | winston.info('getting scorecard stats'); 75 | allStats(function(err, stats){ 76 | res.json(stats); 77 | }) 78 | }; 79 | 80 | Controller.statsOne = function(req, res, next) { 81 | winston.info('getting scorecard stats'); 82 | allStats(function(err, stats){ 83 | res.json(stats[req.params.rid]); 84 | }) 85 | }; 86 | 87 | -------------------------------------------------------------------------------- /controllers/ccda_scorecard_xquery.js: -------------------------------------------------------------------------------- 1 | 2 | function baseX(url, options, done){ 3 | options = hashish.merge({ 4 | username: 'admin', 5 | password: 'admin', 6 | parser: rest.parsers.auto 7 | }, options); 8 | 9 | rest.request('http://localhost:8984/rest/HL7Samples'+url, options) 10 | .on('success', function(data, response){ 11 | console.log('success', response.raw.toString()); 12 | done(null, data); 13 | }) 14 | .on('fail', function(data, response){ 15 | console.log('fail', data); 16 | done(data); 17 | }) 18 | .on('error', function(data, response){ 19 | console.log('error', data); 20 | done(data); 21 | }); 22 | } 23 | 24 | function addDoc(doc, done){ 25 | baseX('/123', { 26 | method: 'put', 27 | data: doc, 28 | headers: {'Content-type': 'application/xml'} 29 | }, 30 | done); 31 | }; 32 | 33 | function query(path, q, done){ 34 | baseX(path, { 35 | method: 'get', 36 | query: { 37 | query: q, 38 | }, 39 | }, 40 | done); 41 | }; 42 | 43 | Controller.query = query; 44 | Controller.addDoc = addDoc; 45 | Controller.baseX = baseX; 46 | 47 | 48 | -------------------------------------------------------------------------------- /controllers/examples.js: -------------------------------------------------------------------------------- 1 | var Controller = module.exports = {}; 2 | 3 | var request = require('request'); 4 | 5 | var ghreq = function(){ 6 | var tags = {}; 7 | var bodies = {}; 8 | 9 | return function(url, cb){ 10 | request.get({ 11 | url: url, 12 | headers: { 13 | "If-None-Match": tags[url], 14 | "User-Agent": "smart-ccda-scorecard" 15 | } 16 | }, function(err, response, body){ 17 | 18 | tags[url] = response.headers["etag"]; 19 | 20 | if (response.statusCode !== 304){ 21 | bodies[url] = body; 22 | console.log("Save result in cache"); 23 | } 24 | 25 | return cb(err, response, bodies[url]); 26 | }) 27 | }; 28 | }(); 29 | 30 | Controller.list = function(req, res, next){ 31 | var url = 'https://api.github.com/repos/chb/sample_ccdas/git/trees/master?recursive=1'; 32 | ghreq(url, function(err, response, body) { 33 | if (err) { 34 | next(err); 35 | } else { 36 | res.setHeader('Content-Type', 'application/json') 37 | res.end(body); 38 | } 39 | }); 40 | }; 41 | 42 | Controller.fetch = function(req, res, next){ 43 | // Hash should be nothing but a hex value 44 | var hash = req.params.id.replace(/[^a-f0-9]/g, ''); 45 | if (hash !== req.params.id){ 46 | err("Invalid file id: " + req.params.id); 47 | } 48 | ghreq('https://api.github.com/repos/chb/sample_ccdas/git/blobs/' + hash, 49 | function(err, response, body) { 50 | if (err) { 51 | return next(err); 52 | } 53 | res.setHeader('Content-Type', 'application/xml') 54 | res.end(new Buffer(JSON.parse(body).content, 'base64').toString()); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /launch.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'); 2 | config.launch(); 3 | -------------------------------------------------------------------------------- /lib/BaseRubric.js: -------------------------------------------------------------------------------- 1 | var common = require('./common'); 2 | 3 | var BaseRubric = module.exports = function(){ }; 4 | 5 | BaseRubric.prototype.runTest = function(input){ 6 | var self = this; 7 | 8 | var score = { 9 | fail: function(){ 10 | self.testing.numerator--; 11 | if (arguments.length > 0) { 12 | var reasons = []; 13 | for (var i=0; i report, histograms 10 | [x] Separate out functionality from the C-CDA receiver 11 | [x] Host online 12 | [x] Build in support for LOINC top2K codes 13 | 14 | [x] Add a tree-path identifying a section/category for each rubric 15 | [x] Convert from letter grades to points, per-rubric 16 | [x] Add a list of "pointsFor": ["ccda-base", "smart"] } 17 | [x] Render clientside table to look more like html5test.com 18 | [x] Redesign client with Angular + Bootstrap 19 | [x] Use % rather than raw points overall! i.e. normalize to 100. 20 | [x] Add "Tweet my score" 21 | [ ] Add 'any value set' membership vs. just the valueset we expect 22 | [ ] like, +1 links 23 | [ ] filters in UI for 'show errors only'. 24 | 25 | */ 26 | 27 | var md = require("node-markdown").Markdown; 28 | 29 | module.exports = { 30 | report: report, 31 | valueSetMembership: valueSetMembership, 32 | extractCodes: extractCodes, 33 | xpath: xpath, 34 | }; 35 | 36 | var DEFAULT_NS = { 37 | "h": "urn:hl7-org:v3", 38 | "xsi": "http://www.w3.org/2001/XMLSchema-instance" 39 | } 40 | 41 | function xpath(doc, p, ns){ 42 | var r= doc.find(p, ns || DEFAULT_NS); 43 | return r; 44 | }; 45 | 46 | function extractCodes(ccda, xpathExpr, vocabService){ 47 | 48 | var ret = []; 49 | 50 | var xpathResult = xpath(ccda, xpathExpr); 51 | 52 | xpathResult.forEach(function(r){ 53 | 54 | var code = { 55 | code : xpath(r, "string(@code)"), 56 | displayName : xpath(r, "string(@displayName)"), 57 | codeSystem : xpath(r, "string(@codeSystem)"), 58 | codeSystemName : xpath(r, "string(@codeSystemName)"), 59 | nullFlavor : xpath(r, "string(@nullFlavor)"), 60 | node: r 61 | }; 62 | 63 | code.normalized = vocabService.lookup(code); 64 | return ret.push(code); 65 | 66 | }); 67 | 68 | return ret; 69 | }; 70 | 71 | function valueSetMembership(ccda, xpathExpr, expectedValueSets, vocabService){ 72 | 73 | var ret = { 74 | inSet: [], 75 | notInSet: [] 76 | }; 77 | 78 | 79 | var codes = extractCodes(ccda, xpathExpr, vocabService); 80 | 81 | codes.forEach(function(code){ 82 | var v = code.normalized; 83 | 84 | var inSet = (expectedValueSets.length === 0); 85 | expectedValueSets.forEach(function(vsName){ 86 | 87 | if (v && v.valueSets.indexOf(vsName) !== -1) 88 | { 89 | inSet = true; 90 | } 91 | }); 92 | 93 | if (inSet){ 94 | ret.inSet.push(code); 95 | } else { 96 | ret.notInSet.push(code); 97 | } 98 | 99 | }); 100 | 101 | return ret; 102 | }; 103 | 104 | var defaultPointRanges = { 105 | 3: [ 106 | [.8, 1], // 3 points 107 | [.5, .8], // 2 points 108 | [0, .5], // 1 point 109 | ], 110 | 1: [ 111 | [1, 1], // 3 points 112 | ] 113 | }; 114 | 115 | function report(rubric, numerator, denominator, reportTemplateArgs){ 116 | 117 | var ret = { 118 | doesNotApply: true, 119 | rubric: rubric.json.id 120 | }; 121 | 122 | if (rubric.reportTemplate){ 123 | ret.detail = rubric.reportTemplate(reportTemplateArgs).trim(); 124 | } 125 | 126 | if (denominator === 0) { 127 | return ret; 128 | } 129 | 130 | delete ret.doesNotApply; 131 | var maxPoints = rubric.json.maxPoints; 132 | 133 | var slices = rubric.json.pointRanges || defaultPointRanges[maxPoints] || []; 134 | console.log(rubric.json.maxPoints, slices); 135 | var rawScore = numerator / denominator; 136 | 137 | ret.score = 0; 138 | if (rawScore === 1) { 139 | ret.score = rubric.json.maxPoints; 140 | } else { 141 | for (var i = 0; i < slices.length; i++){ 142 | var bottom = slices[i][0]; 143 | var top = slices[i][1]; 144 | var score = slices[i][2]; 145 | if (bottom < rawScore && top >= rawScore){ 146 | ret.score = slices.length - i; 147 | break; 148 | } 149 | } 150 | } 151 | 152 | return ret; 153 | }; 154 | 155 | -------------------------------------------------------------------------------- /lib/grader.js: -------------------------------------------------------------------------------- 1 | var Hash = require('hashish'); 2 | var xpath = require('./common').xpath; 3 | var lxml = require("libxmljs"); 4 | var async = require("async"); 5 | var db = require('../config').db; 6 | var rubrics = require('./rubrics'); 7 | 8 | /* 9 | * Calculate a grade report for a given C-CDA. 10 | * 11 | * Callback takes `(err, report)` where `report` 12 | * is a JSON structure containing a complete report. 13 | * 14 | * @param {String} src 15 | * @param {Function} done 16 | * @api public 17 | */ 18 | module.exports = function gradeCcda(params, done){ 19 | var manager = new Manager(params); 20 | 21 | Object.keys(rubrics).forEach(function(r){ 22 | manager.addRubric(rubrics[r]); 23 | }); 24 | manager.report(done); 25 | }; 26 | 27 | function Vocab(codes, sourceCodes){ 28 | this.codes = codes; 29 | this.sourceCodes = sourceCodes; 30 | } 31 | 32 | // yuck - linerar comparison across all in-memory codes :-) 33 | // TODO replace with code lookup hash. 34 | Vocab.prototype.lookup = function(target){ 35 | 36 | var matches = this.codes.filter(function(c){ 37 | return ( 38 | c.code === target.code && 39 | c.codeSystem === target.codeSystem 40 | ); 41 | }); 42 | 43 | if (matches.length > 1) { 44 | throw new Error("Multiple vocab matches for " + JSON.stringify(code)); 45 | } 46 | 47 | if (matches.length === 0) { 48 | return null; 49 | } 50 | 51 | return matches[0]; 52 | }; 53 | 54 | function Manager(params){ 55 | 56 | this.ccdaSrc = params.src; 57 | this.saveFlag = params.save; 58 | 59 | try { 60 | this.ccda = lxml.parseXmlString(this.ccdaSrc); 61 | } catch(e) { 62 | var emsg = e.message.replace("\n", "") + " " + JSON.stringify(e); 63 | throw emsg; 64 | } 65 | this.rubrics = {}; 66 | this._auto = {}; 67 | this._auto.vocab = [this._fetchVocab.bind(this)]; 68 | this._auto.report = [this._report.bind(this)]; 69 | this._auto.logger = ["report", this._logger.bind(this)]; 70 | }; 71 | 72 | 73 | /* 74 | * Ensure that grading functions have synchronous 75 | * access to concept / value set membership for 76 | * all codes in the C-CDA document (by pre-fetching). 77 | */ 78 | Manager.prototype._fetchVocab = function(done, results) { 79 | 80 | var codeBank = {}; 81 | var manager = this; 82 | 83 | var allCodes = xpath(this.ccda, "//*[boolean(./@code) and boolean(./@codeSystem)]"); 84 | allCodes.forEach(function(c){ 85 | var code = xpath(c, "string(@code)"); 86 | var displayName = xpath(c, "string(@displayName)"); 87 | var codeSystemName = xpath(c, "string(@codeSystemName)"); 88 | var system = xpath(c, "string(@codeSystem)"); 89 | var key = system + "/" + code; 90 | 91 | codeBank[key] = { 92 | code: code, 93 | codeSystem: system, 94 | codeSystemName: codeSystemName, 95 | displayName: displayName 96 | }; 97 | 98 | }); 99 | 100 | var codes = Object.keys(codeBank).map(function(k){return codeBank[k];}); 101 | 102 | db.vocab.collection("concepts", function(err, concepts){ 103 | if (err){ 104 | return done(err); 105 | } 106 | var orq = codes.map(function(c){ 107 | return { 108 | "code": c.code, 109 | "codeSystem": c.codeSystem 110 | }; 111 | }); 112 | 113 | var q = {"$or": orq}; 114 | concepts.find(q).toArray(function(err, vals){ 115 | manager.vocab = new Vocab(vals, codes); 116 | console.log("Looked up codes: ", vals.length); 117 | done(err, vals); 118 | }); 119 | }); 120 | 121 | }; 122 | 123 | // fire-and-forget logging of raw C-CDA and 124 | // update of the overall score histograms 125 | Manager.prototype._logger = function(done, results) { 126 | var manager = this; 127 | 128 | db.ccdaScorecard.collection("scoreStats", function(err, stats){ 129 | if (err){ 130 | return done(err); 131 | } 132 | 133 | results.report.forEach(function(score){ 134 | var inc = {}; 135 | 136 | var key = score.doesNotApply ? "N/A" : score.score; 137 | inc["counts."+key] = 1; 138 | 139 | 140 | stats.update( 141 | {_id: score.rubric}, 142 | {$inc: inc}, 143 | {upsert: true}, function(err){ 144 | if(err){ 145 | console.log("updated scores err", err); 146 | } 147 | }); 148 | }); 149 | }); 150 | 151 | if (manager.saveFlag){ 152 | db.ccdaScorecard.collection("ccdas", function(err, ccdas){ 153 | if (err){ 154 | console.log("Couldn't access ccda collection", err); 155 | return done(err); 156 | } 157 | ccdas.insert({raw: manager.ccdaSrc, time: new Date()}, {}, function(err){ 158 | if(err){ 159 | console.log("pushed doc err", err); 160 | } 161 | }); 162 | }); 163 | } 164 | 165 | done(null); 166 | }; 167 | 168 | Manager.prototype._report = function(done, results) { 169 | var manager = this; 170 | var reports = []; 171 | 172 | this._auto["report"].slice(0,-1).forEach(function(depName){ 173 | reports.push(results[depName]); 174 | }); 175 | 176 | done(null, reports); 177 | }; 178 | 179 | Manager.prototype.addRubric = function(rubric){ 180 | var manager = this; 181 | 182 | var r = this.rubrics[rubric.json.id] = new rubric(); 183 | r.manager = manager; 184 | 185 | var deps = (r.constructor.dependencies || []).slice(); 186 | deps.unshift("vocab"); 187 | deps.unshift("report"); 188 | 189 | console.log("Binding"); 190 | console.log(r.constructor.json.id, "ID bound"); 191 | manager._addDependency("report", r.constructor.json.id+".report", r.report.bind(r)); 192 | manager._addDependency(r.constructor.json.id+".report", "vocab"); 193 | 194 | }; 195 | 196 | Manager.prototype._addDependency = function(needer, needed, fn){ 197 | if (fn) { 198 | this._auto[needed] = [fn]; 199 | } 200 | 201 | if (!this._auto[needer]){ 202 | throw new Error("Unknown dependency: " + needer); 203 | } 204 | 205 | this._auto[needer].unshift(needed); 206 | }; 207 | 208 | Manager.prototype.report = function(done){ 209 | var manager = this; 210 | 211 | async.auto(this._auto, function(err, results){ 212 | done(err, results.report); 213 | }) 214 | }; 215 | 216 | -------------------------------------------------------------------------------- /lib/rubrics.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var ejs = require('ejs'); 4 | var util = require('util'); 5 | var BaseRubric = require('./BaseRubric'); 6 | 7 | /* 8 | * Discover rubrics on-disk. These have a consistent format 9 | * index.js defining the grade function, and rubric.json 10 | * providing the static definition with grade cut-offs. 11 | */ 12 | var defaultTemplateFile = path.join( 13 | __dirname, '..','rubrics','partials','mistakes.ejs' 14 | ); 15 | var defaultTemplate = fs.readFileSync(defaultTemplateFile).toString(); 16 | defaultTemplate = ejs.compile(defaultTemplate, {filename:defaultTemplateFile }); 17 | 18 | var rubricDir = path.join(__dirname, "..", "rubrics"); 19 | var rubricArray = fs.readdirSync(rubricDir) 20 | .filter(function(d){ 21 | return (d !== "common"); 22 | }) 23 | .filter(function(d){ 24 | return (d.match(/\.js$/)); 25 | }) 26 | .map(function(d){ 27 | 28 | var rubric = d.match(/(.*)\.js$/)[1]; 29 | var ret = require(path.join(rubricDir, rubric)); 30 | 31 | var p = ret.prototype; 32 | util.inherits(ret, BaseRubric); 33 | 34 | Object.keys(p).forEach(function(k){ 35 | ret.prototype[k] =p[k]; 36 | }); 37 | 38 | ret.json = require(path.join(rubricDir, rubric+".json")); 39 | ret.json.id = rubric; 40 | 41 | var reportFile = path.join(rubricDir, rubric+".report.ejs"); 42 | if (fs.existsSync(reportFile)) { 43 | var template = fs.readFileSync(reportFile).toString(); 44 | ret.reportTemplate = ejs.compile(template, {filename: reportFile}); 45 | } else { 46 | ret.reportTemplate = defaultTemplate; 47 | } 48 | 49 | return ret; 50 | }); 51 | 52 | module.exports = rubrics = {}; 53 | rubricArray.forEach(function(r){ 54 | rubrics[r.json.id] = r; 55 | }); 56 | 57 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ccda-receiver", 3 | "version": "0.0.1", 4 | "description": "Import CCDA -> JSON. Then serve resources over a RESTful API", 5 | "author": "Josh Mandel ", 6 | "scripts":{ 7 | "start": "node ./launch.js" 8 | }, 9 | "dependencies": { 10 | "node-markdown": "*", 11 | "bson": "*", 12 | "connect-powered-by": "0.1.x", 13 | "xml2js": "*", 14 | "async": "*", 15 | "hashish": "*", 16 | "request": "*", 17 | "express": "*", 18 | "mongodb": "1.1.11", 19 | "libxmljs":"*", 20 | "xdate":"*", 21 | "optimist":"*", 22 | "mocha":"*", 23 | "should":"*", 24 | "node-uuid":"*", 25 | "underscore":"*", 26 | "jade": "*", 27 | "ejs": "*", 28 | "passport":"*", 29 | "passport-browserid":"*", 30 | "passport-http-bearer":"*", 31 | "oauth2orize":"*", 32 | "stylus": "*", 33 | "mongoose": "*", 34 | "winston": "*" 35 | }, 36 | "devDependencies": { 37 | }, 38 | "engine": "node >= 0.6.0" 39 | } 40 | -------------------------------------------------------------------------------- /public/angular-ui/ui-bootstrap-custom-0.4.0.js: -------------------------------------------------------------------------------- 1 | angular.module("ui.bootstrap", ["ui.bootstrap.dropdownToggle"]); 2 | /* 3 | * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js 4 | * @restrict class or attribute 5 | * @example: 6 | 14 | */ 15 | 16 | angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) { 17 | var openElement = null, 18 | closeMenu = angular.noop; 19 | return { 20 | restrict: 'CA', 21 | link: function(scope, element, attrs) { 22 | scope.$watch('$location.path', function() { closeMenu(); }); 23 | element.parent().bind('click', function() { closeMenu(); }); 24 | element.bind('click', function (event) { 25 | 26 | var elementWasOpen = (element === openElement); 27 | 28 | event.preventDefault(); 29 | event.stopPropagation(); 30 | 31 | if (!!openElement) { 32 | closeMenu(); 33 | } 34 | 35 | if (!elementWasOpen) { 36 | element.parent().addClass('open'); 37 | openElement = element; 38 | closeMenu = function (event) { 39 | if (event) { 40 | event.preventDefault(); 41 | event.stopPropagation(); 42 | } 43 | $document.unbind('click', closeMenu); 44 | element.parent().removeClass('open'); 45 | closeMenu = angular.noop; 46 | openElement = null; 47 | }; 48 | $document.bind('click', closeMenu); 49 | } 50 | }); 51 | } 52 | }; 53 | }]); -------------------------------------------------------------------------------- /public/bootstrap/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /public/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chb/ccdaScorecard/a6e6641dd529d92147fb330a381c56f6d39654c2/public/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chb/ccdaScorecard/a6e6641dd529d92147fb330a381c56f6d39654c2/public/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/ccdaScorecard/controllers.js: -------------------------------------------------------------------------------- 1 | angular.module('ccdaScorecard', ['ui.bootstrap', 'ngResource'], function($routeProvider, $locationProvider){ 2 | 3 | $routeProvider.when('/', { 4 | templateUrl:'/static/ccdaScorecard/templates/index.html', 5 | controller: 'MainController' 6 | }) 7 | 8 | $routeProvider.when('/static/ccdaScorecard/', { 9 | templateUrl:'/static/ccdaScorecard/templates/index.html', 10 | controller: 'MainController' 11 | }) 12 | 13 | $routeProvider.otherwise({ 14 | templateUrl:'/static/ccdaScorecard/templates/index.html', 15 | controller: 'MainController' 16 | }) 17 | 18 | // $locationProvider.html5Mode(true); 19 | }); 20 | 21 | angular.module('ccdaScorecard').filter('ccdas', function(){ 22 | return function(list) { 23 | var ret = list.filter(function(entry){ 24 | if (entry.path.match(/EMERGE\/Patient-\d\d/)) return false; 25 | return entry.path.match(/(xml)|(txt)|(ccd)/i); 26 | }); 27 | return ret; 28 | }; 29 | }); 30 | 31 | angular.module('ccdaScorecard').factory('GithubExamples', function($resource, $http, $q) { 32 | var examples = {tree: []}; 33 | 34 | // pre-fetch list of examples on load 35 | $http({method: 'GET', url: '/v1/examples/'}).success(function(data, status, headers, config){ 36 | examples.url = data.url; 37 | examples.sha = data.sha; 38 | examples.tree = data.tree; 39 | }); 40 | 41 | return { 42 | all: examples, 43 | one: function(id){ 44 | var deferred = $q.defer(); 45 | $http({method: 'GET', url: '/v1/examples/'+id}).success( function(data, status, headers, config){ 46 | deferred.resolve(data); 47 | }); 48 | return deferred.promise; 49 | } 50 | }; 51 | }); 52 | 53 | 54 | angular.module('ccdaScorecard').factory('Scorecard', function($resource, $http) { 55 | 56 | 57 | // Resource to fetch data for interpreting / calculating scores. 58 | // TODO: abstract this into a service when we have >1 controller. 59 | var Scorecard = $resource('/v1/ccda-scorecard/:res', {}, { 60 | rubrics: {method:'GET', params: {res: "rubrics"}}, 61 | stats: {method:'GET', params: {res: "stats"}}, 62 | }); 63 | 64 | // simulate a `request` method because ng's $resource doesn't (yet) 65 | // support custom headers or explicit Content-type. 66 | Scorecard.request = function(_, data, scores, errors) { 67 | 68 | scores.length = 0; 69 | errors.length = 0; 70 | 71 | $http({ 72 | method: "POST", 73 | data: data, 74 | url: "/v1/ccda-scorecard/request?example="+_.isExample, 75 | headers: {"Content-Type": "application/x-www-form-urlencoded"} 76 | }).success(function(r){ 77 | for (var i = 0; i < r.length; i++){ 78 | scores.push(r[i]); 79 | } 80 | }).error(function(e){ 81 | errors.push(e); 82 | }); 83 | return; 84 | } 85 | 86 | // ng's $resource can't grab a single string except to treat it 87 | // as a character array... so we do this the old-fashioned way, too. 88 | Scorecard.getExample = function($scope) { 89 | $http({method: 'GET', url: '/static/ccdaScorecard/example.ccda.xml'}) 90 | .success(function(data, status, headers, config) { 91 | $scope.example = data; 92 | }); 93 | return ""; 94 | } 95 | 96 | Scorecard.rubrics = Scorecard.rubrics(); 97 | Scorecard.stats = Scorecard.stats(); 98 | 99 | return Scorecard; 100 | }); 101 | 102 | angular.module('ccdaScorecard').controller("ScoreController", 103 | function($scope, Scorecard) { 104 | 105 | $scope.$on("expandRequest", function(e, state){ 106 | $scope.showDetails = state; 107 | }); 108 | 109 | var rubric = $scope.rubric = Scorecard.rubrics[$scope.score.rubric]; 110 | 111 | $scope.cssClass = function(){ 112 | var score = $scope.score.score; 113 | var max = rubric.maxPoints; 114 | if (score === max){return "success";} 115 | else if (score ===0) {return "error";} 116 | return "warning"; 117 | }; 118 | 119 | $scope.showDetails = true; 120 | } 121 | ); 122 | 123 | angular.module('ccdaScorecard').controller("MainController", 124 | function($scope, Scorecard, GithubExamples, $timeout) { 125 | 126 | $scope.dropChoices = [{href: "#", text: "wdfa"}]; 127 | 128 | $scope.stats = Scorecard.stats; 129 | $scope.rubrics = Scorecard.rubrics; 130 | $scope.example = Scorecard.getExample($scope); 131 | $scope.submission = $scope.current = ""; 132 | $scope.getScore = function(){ 133 | $scope.scoring = true; 134 | var toSubmit = $scope.submission.trim(); 135 | 136 | $scope.scores =[]; 137 | $scope.errors = []; 138 | 139 | Scorecard.request({ isExample:( 140 | toSubmit === $scope.example.trim() || 141 | toSubmit === $scope.current.trim() 142 | )}, toSubmit, $scope.scores, $scope.errors ); 143 | 144 | }; 145 | 146 | $scope.expandAllScores = function(){$scope.$broadcast("expandRequest", true);}; 147 | $scope.collapseAllScores = function(){$scope.$broadcast("expandRequest", false);}; 148 | $scope.githubFiles = GithubExamples.all; 149 | $scope.pickExample = function(example){ 150 | $scope.submission = "fetching sample..."; 151 | 152 | GithubExamples.one(example.sha).then(function(content){ 153 | $scope.submission = $scope.current = content; 154 | $scope.getScore(); 155 | }); 156 | 157 | }; 158 | 159 | function parseSections(scoreList){ 160 | var sections = {}, ret = []; 161 | 162 | for (var i = 0; i < scoreList.length; i++) { 163 | var score = scoreList[i]; 164 | var sectionName = $scope.rubrics[score.rubric].category[0]; 165 | (sections[sectionName] || (sections[sectionName] = [])).push(score); 166 | }; 167 | 168 | 169 | var overallPoints = 0; 170 | var overallMaxPoints = 0; 171 | 172 | Object.keys(sections).sort().forEach(function(k){ 173 | 174 | sections[k] = sections[k].sort(function(a,b){ 175 | return a.rubric === b.rubric ? 0 : a.rubric < b.rubric ? -1 : 1; 176 | }); 177 | 178 | var section = { 179 | name: k, 180 | scores: sections[k] 181 | }; 182 | 183 | var sectionPoints = 0; 184 | var sectionMaxPoints = 0; 185 | 186 | section.scores.forEach(function(s){ 187 | if (s.doesNotApply) { 188 | return; 189 | } 190 | sectionPoints += s.score; 191 | overallPoints += s.score; 192 | sectionMaxPoints += $scope.rubrics[s.rubric].maxPoints; 193 | overallMaxPoints += $scope.rubrics[s.rubric].maxPoints; 194 | }); 195 | 196 | if (sectionMaxPoints === 0){ 197 | section.percent = null; 198 | } else { 199 | section.percent = parseInt(100 * sectionPoints / sectionMaxPoints); 200 | } 201 | ret.push(section); 202 | }); 203 | 204 | $scope.overallPercent = parseInt(100 * overallPoints / overallMaxPoints); 205 | 206 | return ret; 207 | 208 | } 209 | 210 | $scope.$watch("scores", function(scores){ 211 | if (!scores || scores.length == 0) return; 212 | $scope.scoring = false; 213 | $scope.scoreSections = parseSections(scores); 214 | }, true); 215 | 216 | $scope.loading = function(){ 217 | var ret = ( 218 | $scope.example.length == 0 || 219 | Object.keys($scope.rubrics).length == 0 220 | ); 221 | return ret; 222 | } 223 | 224 | $scope.showDetails = function(score){ 225 | score.showDetails = true; 226 | }; 227 | 228 | if(window.defaultCcda !== null){ 229 | $scope.submission = decodeURIComponent(window.defaultCcda); 230 | $scope.$watch("loading()",function(newVal){ 231 | console.log("loading", newVal); 232 | if (newVal === true) 233 | $scope.getScore(); 234 | }); 235 | } 236 | 237 | 238 | } 239 | ); 240 | 241 | angular.module('ccdaScorecard') 242 | .directive('statsHistogram', function($timeout, dateFilter) { 243 | // return the directive link function. (compile function not needed) 244 | return { 245 | restrict: 'C', 246 | scope: {distribution: '='}, 247 | link: function(scope, element, attrs) { 248 | // A formatter for counts. 249 | console.log("Linking histogram"); 250 | 251 | var formatCount = d3.format(",.0f"); 252 | 253 | var margin = { 254 | left: 40, 255 | right: 40 256 | } 257 | 258 | var dim = { 259 | width: 150, 260 | barHeight: 20 261 | }; 262 | 263 | 264 | function makeHistogram(scores) { 265 | console.log("making histogram", element); 266 | var scoreKeys = Object.keys(scores).sort().reverse(); 267 | 268 | var scoreArray = scoreKeys.map(function(k){ 269 | return scores[k]; 270 | }); 271 | 272 | angular.element(element).text(""); 273 | var bounding = d3.select(element[0]).append("svg") 274 | .attr("width", dim.width) 275 | .attr("class", "chart") 276 | .attr("height", dim.barHeight * scoreArray.length); 277 | 278 | var chart =bounding.append("g") 279 | .attr("transform", "translate(" + margin.left + "," + 0 + ")"); 280 | 281 | var x = d3.scale.linear() 282 | .domain([0, d3.max(scoreArray)]) 283 | .range([0, dim.width - margin.left - margin.right]); 284 | 285 | chart.selectAll("rect") 286 | .data(scoreKeys) 287 | .enter().append("rect") 288 | .attr("y", function(d, i) { return i * dim.barHeight; }) 289 | .attr("width", function(d){return x(scores[d]);}) 290 | .attr("height", dim.barHeight); 291 | 292 | bounding.selectAll("line") 293 | .data(scoreKeys) 294 | .enter().append("line") 295 | .attr("y1", function(d, i) { return (i+.5) * dim.barHeight; }) 296 | .attr("y2", function(d, i) { return (i+.5) * dim.barHeight; }) 297 | .attr("x1", function(d, i) { return margin.left-2 }) 298 | .attr("x2", function(d, i) { return margin.left+2; }); 299 | 300 | chart.selectAll("text.barlen") 301 | .data(scoreKeys).enter() 302 | .append("text") 303 | .attr("class", "barlen") 304 | .attr("dy", ".35em") 305 | .attr("y", function(d, i) { return (i+0.5) * dim.barHeight; }) 306 | .attr("x", function(d){return x(scores[d])+2;}) 307 | .attr("text-anchor", "beginning").text(function(d) { return formatCount(scores[d]); }); 308 | 309 | bounding.selectAll("text.barLabel") 310 | .data(scoreKeys) 311 | .enter().append("text") 312 | .attr("class", "barLabel") 313 | .attr("dy", ".35em") 314 | .attr("y", function(d, i) { return (i+0.5) * dim.barHeight; }) 315 | .attr("x", margin.left-2) 316 | .attr("text-anchor", "end") 317 | .text(function(d){ 318 | if (isNaN(parseInt(d))) return d; 319 | return d + " pts"; 320 | }); 321 | 322 | }; 323 | 324 | // element.text(""); 325 | // watch the expression, and update the UI on change. 326 | scope.$watch('distribution', function(value, oldval) { 327 | makeHistogram(value); 328 | }, false); 329 | 330 | } 331 | }; 332 | }); 333 | 334 | angular.module('ccdaScorecard') 335 | .directive('tweetButton', function($timeout, dateFilter) { 336 | // return the directive link function. (compile function not needed) 337 | return { 338 | restrict: 'AE', 339 | scope: {score: '@'}, 340 | link: function(scope, element, attrs) { 341 | // A formatter for counts. 342 | scope.$watch("score", function(newScore){ 343 | if (typeof twttr === "undefined") return; 344 | var score = attrs.score; 345 | var origin = window.location.protocol + "//" + window.location.host; 346 | if (window.location.port == "80" || window.location.port == "443") { 347 | origin = origin.replace(/\:.*/,""); 348 | } 349 | element.html(''); 350 | twttr.widgets.load(); 351 | }); 352 | } 353 | }; 354 | }); 355 | 356 | angular.module('ccdaScorecard') 357 | .directive('scrollIf', function () { 358 | return function (scope, element, attributes) { 359 | scope.$watch(attributes.scrollIf, function(v, old){ 360 | if (scope.$eval(attributes.scrollIf)) { 361 | window.setTimeout(function(){ 362 | $.scrollTo(element, 200, {axis: 'y', offset: -50}); 363 | }, 0); 364 | } 365 | }); 366 | } 367 | }); 368 | -------------------------------------------------------------------------------- /public/ccdaScorecard/histo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 26 | 105 | -------------------------------------------------------------------------------- /public/ccdaScorecard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SMART C-CDA Scorecard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 29 |
30 | 31 |
32 |
Loading...
33 |
34 | 35 | 36 | 37 |
38 |
39 |

Help improve the Scorecard!

40 |

Four ways to help improve the SMART C-CDA Scorecard:

41 |
    42 |
  1. Try out the Scorecard and tweet about it@SMARTHealthIT
  2. 43 |
  3. Share your sample C-CDA documents with the public
  4. 44 |
  5. Suggest new rubrics, or improvements to the existing ones
  6. 45 |
  7. Contribute code to improve the Scorecard
  8. 46 |
47 |
48 |

About the Scorecard

49 | 50 |

51 | The SMART C-CDA Scorecard promotes best practices in 52 | C-CDA implementation by assessing key aspects of the structured data found 53 | in invididual documents. It's a tool to help implementers gain visibility 54 | into how well and how often best practices are followed — and also to 55 | summarize progress with a rough quantitative assessment, 56 | highlighting improvements that can be made today. 57 |

58 | 59 |

Fills gaps and complements official validation tools

60 | 61 |

The Scorecard runs alongside official C-CDA validation tools like the Transport Testing 63 | Tool provided by NIST and Model-Driven Health Tools. The official validation tools provide comprehensive 66 | assessment of syntactic conformance to the C-CDA specification, but they 67 | don't always enforce higher-level best practices in C-CDA implementation. 68 | The Scorecard fills gaps and provides visibility into 69 | constraints missing from the official specifications, or features missing 70 | from the official validation tools. 71 | 72 | Our initial focus is on value set membership for three key vocabularies used in C-CDA: LOINC, RxNorm, SNOMED CT. 73 | 74 |

75 | 76 |

Powered by SMART Platforms

77 | Visit smartplatforms.org to learn more about the ONC-funded, SHARP III project. 78 |
79 |
80 |
81 |

© Harvard Medical School / Boston Children's Hospital, 2013. Source on GitHub.

82 |
83 |
84 | 85 | 103 | 104 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 120 | 121 | 135 | 136 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /public/ccdaScorecard/modules.js: -------------------------------------------------------------------------------- 1 | angular.module('ccdaReceiver', ['ui.bootstrap', 'smartBox'], function($routeProvider, $locationProvider){ 2 | 3 | $routeProvider.when('/ui/select-patient', { 4 | templateUrl:'/static/ccdaReceiver/templates/select-patient.html', 5 | reloadOnSearch:false 6 | }) 7 | 8 | $routeProvider.when('/ui', {redirectTo:'/ui/select-patient'}); 9 | 10 | $routeProvider.when('/ui/patient-selected/:pid', { 11 | templateUrl:'/static/ccdaReceiver/templates/patient-selected.html', 12 | }); 13 | 14 | $routeProvider.when('/ui/authorize', { 15 | templateUrl:'/static/ccdaReceiver/templates/authorize-app.html', 16 | }); 17 | 18 | $locationProvider.html5Mode(true); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /public/ccdaScorecard/templates/authorize-app.html: -------------------------------------------------------------------------------- 1 | 
2 |

!
wants permission to:

3 |
4 |
5 |
6 | 7 |
8 | ... 9 |
10 |
11 |
12 |

Connect as you

13 | Query the SMART CCDA Receiver for patient data on your behalf. 14 |
15 |
16 |

Access a single patient record:

17 | 18 |
19 |
20 | 21 |
22 |
23 | Hmm 24 |
25 |
26 |
27 | 28 |
29 | 30 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /public/ccdaScorecard/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 5 | Your C-CDA. Beautiful.

6 |
7 | 8 | 9 |
10 |
11 | 12 |

Paste and go.

13 | 14 |
18 | 19 | 26 | 27 | 28 | 29 | Show me an example 30 | 31 | 32 | 39 | 40 | loading...
41 |
42 |
43 | 44 |
45 | {{errors[0]}} 46 |
47 | 48 |
49 |
Scoring...
50 | 51 |
52 |

53 | Your C-CDA's overall score: {{overallPercent}}% 54 |

55 | 56 |
57 | 58 | 63 |
64 | Collapse all 65 | Expand all 66 | 67 |
68 |
69 | 70 |
71 |
72 |
73 | 74 |

{{section.name}}

75 | 76 |

77 | {{section.percent}}% 78 | N/A 79 |

80 | 81 | 82 | 83 | 84 | 85 | 86 | 91 | 101 | 102 | 103 | 104 | 126 | 127 | 128 | 129 |
87 | 88 | 89 | {{rubric.description}} 90 | 92 | 93 | 94 | N/A 95 | 96 | 97 | {{score.score}}/{{rubric.maxPoints}} points 98 | 99 | 100 |
105 |
106 | 112 | 113 |
114 |

Best Practice: {{rubric.detail}}

115 |
116 | 117 |
118 | Your Results: 119 | 120 |
121 | 122 |
123 | 124 |
125 |
130 |
131 | 132 |
133 |
134 | 135 | -------------------------------------------------------------------------------- /public/ccdaScorecard/templates/patient-search.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Search for patients by keyword (name, gender, date, city, zip, dob, mrn)
4 |
5 |
6 |
7 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 37 | 38 |
25 | 26 |
32 | 33 | No matches -- try broadening your search. 34 | 35 |   36 |
39 | 40 | 53 | 54 |
55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /public/ccdaScorecard/templates/patient-selected.html: -------------------------------------------------------------------------------- 1 | 
2 |

2
App time for {{patientHelper.name(patient)}}

3 |
4 |
5 | 31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /public/ccdaScorecard/templates/select-patient.html: -------------------------------------------------------------------------------- 1 | 
2 |

1
Choose a patient

3 | 4 |
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /public/images/smart-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chb/ccdaScorecard/a6e6641dd529d92147fb330a381c56f6d39654c2/public/images/smart-logo.png -------------------------------------------------------------------------------- /public/js/modernizr.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.6.2 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-svg 3 | */ 4 | ;window.Modernizr=function(a,b,c){function u(a){i.cssText=a}function v(a,b){return u(prefixes.join(a+";")+(b||""))}function w(a,b){return typeof a===b}function x(a,b){return!!~(""+a).indexOf(b)}function y(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:w(f,"function")?f.bind(d||b):f}return!1}var d="2.6.2",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={svg:"http://www.w3.org/2000/svg"},m={},n={},o={},p=[],q=p.slice,r,s={}.hasOwnProperty,t;!w(s,"undefined")&&!w(s.call,"undefined")?t=function(a,b){return s.call(a,b)}:t=function(a,b){return b in a&&w(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=q.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(q.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(q.call(arguments)))};return e}),m.svg=function(){return!!b.createElementNS&&!!b.createElementNS(l.svg,"svg").createSVGRect};for(var z in m)t(m,z)&&(r=z.toLowerCase(),e[r]=m[z](),p.push((e[r]?"":"no-")+r));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)t(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},u(""),h=j=null,e._version=d,e}(this,this.document); 5 | -------------------------------------------------------------------------------- /public/js/scrollTo.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery.ScrollTo 3 | * Copyright (c) 2007-2013 Ariel Flesler - afleslergmailcom | http://flesler.blogspot.com 4 | * Dual licensed under MIT and GPL. 5 | * 6 | * @projectDescription Easy element scrolling using jQuery. 7 | * http://flesler.blogspot.com/2007/10/jqueryscrollto.html 8 | * @author Ariel Flesler 9 | * @version 1.4.6 10 | * 11 | * @id jQuery.scrollTo 12 | * @id jQuery.fn.scrollTo 13 | * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements. 14 | * The different options for target are: 15 | * - A number position (will be applied to all axes). 16 | * - A string position ('44', '100px', '+=90', etc ) will be applied to all axes 17 | * - A jQuery/DOM element ( logically, child of the element to scroll ) 18 | * - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc ) 19 | * - A hash { top:x, left:y }, x and y can be any kind of number/string like above. 20 | * - A percentage of the container's dimension/s, for example: 50% to go to the middle. 21 | * - The string 'max' for go-to-end. 22 | * @param {Number, Function} duration The OVERALL length of the animation, this argument can be the settings object instead. 23 | * @param {Object,Function} settings Optional set of settings or the onAfter callback. 24 | * @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'. 25 | * @option {Number, Function} duration The OVERALL length of the animation. 26 | * @option {String} easing The easing method for the animation. 27 | * @option {Boolean} margin If true, the margin of the target element will be deducted from the final position. 28 | * @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }. 29 | * @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes. 30 | * @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends. 31 | * @option {Function} onAfter Function to be called after the scrolling ends. 32 | * @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends. 33 | * @return {jQuery} Returns the same jQuery object, for chaining. 34 | * 35 | * @desc Scroll to a fixed position 36 | * @example $('div').scrollTo( 340 ); 37 | * 38 | * @desc Scroll relatively to the actual position 39 | * @example $('div').scrollTo( '+=340px', { axis:'y' } ); 40 | * 41 | * @desc Scroll using a selector (relative to the scrolled element) 42 | * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } ); 43 | * 44 | * @desc Scroll to a DOM element (same for jQuery object) 45 | * @example var second_child = document.getElementById('container').firstChild.nextSibling; 46 | * $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){ 47 | * alert('scrolled!!'); 48 | * }}); 49 | * 50 | * @desc Scroll on both axes, to different values 51 | * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } ); 52 | */ 53 | 54 | ;(function( $ ){ 55 | 56 | var $scrollTo = $.scrollTo = function( target, duration, settings ){ 57 | $(window).scrollTo( target, duration, settings ); 58 | }; 59 | 60 | $scrollTo.defaults = { 61 | axis:'xy', 62 | duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 1, 63 | limit:true 64 | }; 65 | 66 | // Returns the element that needs to be animated to scroll the window. 67 | // Kept for backwards compatibility (specially for localScroll & serialScroll) 68 | $scrollTo.window = function( scope ){ 69 | return $(window)._scrollable(); 70 | }; 71 | 72 | // Hack, hack, hack :) 73 | // Returns the real elements to scroll (supports window/iframes, documents and regular nodes) 74 | $.fn._scrollable = function(){ 75 | return this.map(function(){ 76 | var elem = this, 77 | isWin = !elem.nodeName || $.inArray( elem.nodeName.toLowerCase(), ['iframe','#document','html','body'] ) != -1; 78 | 79 | if( !isWin ) 80 | return elem; 81 | 82 | var doc = (elem.contentWindow || elem).document || elem.ownerDocument || elem; 83 | 84 | return /webkit/i.test(navigator.userAgent) || doc.compatMode == 'BackCompat' ? 85 | doc.body : 86 | doc.documentElement; 87 | }); 88 | }; 89 | 90 | $.fn.scrollTo = function( target, duration, settings ){ 91 | if( typeof duration == 'object' ){ 92 | settings = duration; 93 | duration = 0; 94 | } 95 | if( typeof settings == 'function' ) 96 | settings = { onAfter:settings }; 97 | 98 | if( target == 'max' ) 99 | target = 9e9; 100 | 101 | settings = $.extend( {}, $scrollTo.defaults, settings ); 102 | // Speed is still recognized for backwards compatibility 103 | duration = duration || settings.duration; 104 | // Make sure the settings are given right 105 | settings.queue = settings.queue && settings.axis.length > 1; 106 | 107 | if( settings.queue ) 108 | // Let's keep the overall duration 109 | duration /= 2; 110 | settings.offset = both( settings.offset ); 111 | settings.over = both( settings.over ); 112 | 113 | return this._scrollable().each(function(){ 114 | // Null target yields nothing, just like jQuery does 115 | if (target == null) return; 116 | 117 | var elem = this, 118 | $elem = $(elem), 119 | targ = target, toff, attr = {}, 120 | win = $elem.is('html,body'); 121 | 122 | switch( typeof targ ){ 123 | // A number will pass the regex 124 | case 'number': 125 | case 'string': 126 | if( /^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ) ){ 127 | targ = both( targ ); 128 | // We are done 129 | break; 130 | } 131 | // Relative selector, no break! 132 | targ = $(targ,this); 133 | if (!targ.length) return; 134 | case 'object': 135 | // DOMElement / jQuery 136 | if( targ.is || targ.style ) 137 | // Get the real position of the target 138 | toff = (targ = $(targ)).offset(); 139 | } 140 | $.each( settings.axis.split(''), function( i, axis ){ 141 | var Pos = axis == 'x' ? 'Left' : 'Top', 142 | pos = Pos.toLowerCase(), 143 | key = 'scroll' + Pos, 144 | old = elem[key], 145 | max = $scrollTo.max(elem, axis); 146 | 147 | if( toff ){// jQuery / DOMElement 148 | attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] ); 149 | 150 | // If it's a dom element, reduce the margin 151 | if( settings.margin ){ 152 | attr[key] -= parseInt(targ.css('margin'+Pos)) || 0; 153 | attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0; 154 | } 155 | 156 | attr[key] += settings.offset[pos] || 0; 157 | 158 | if( settings.over[pos] ) 159 | // Scroll to a fraction of its width/height 160 | attr[key] += targ[axis=='x'?'width':'height']() * settings.over[pos]; 161 | }else{ 162 | var val = targ[pos]; 163 | // Handle percentage values 164 | attr[key] = val.slice && val.slice(-1) == '%' ? 165 | parseFloat(val) / 100 * max 166 | : val; 167 | } 168 | 169 | // Number or 'number' 170 | if( settings.limit && /^\d+$/.test(attr[key]) ) 171 | // Check the limits 172 | attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max ); 173 | 174 | // Queueing axes 175 | if( !i && settings.queue ){ 176 | // Don't waste time animating, if there's no need. 177 | if( old != attr[key] ) 178 | // Intermediate animation 179 | animate( settings.onAfterFirst ); 180 | // Don't animate this axis again in the next iteration. 181 | delete attr[key]; 182 | } 183 | }); 184 | 185 | animate( settings.onAfter ); 186 | 187 | function animate( callback ){ 188 | $elem.animate( attr, duration, settings.easing, callback && function(){ 189 | callback.call(this, targ, settings); 190 | }); 191 | }; 192 | 193 | }).end(); 194 | }; 195 | 196 | // Max scrolling position, works on quirks mode 197 | // It only fails (not too badly) on IE, quirks mode. 198 | $scrollTo.max = function( elem, axis ){ 199 | var Dim = axis == 'x' ? 'Width' : 'Height', 200 | scroll = 'scroll'+Dim; 201 | 202 | if( !$(elem).is('html,body') ) 203 | return elem[scroll] - $(elem)[Dim.toLowerCase()](); 204 | 205 | var size = 'client' + Dim, 206 | html = elem.ownerDocument.documentElement, 207 | body = elem.ownerDocument.body; 208 | 209 | return Math.max( html[scroll], body[scroll] ) 210 | - Math.min( html[size] , body[size] ); 211 | }; 212 | 213 | function both( val ){ 214 | return typeof val == 'object' ? val : { top:val, left:val }; 215 | }; 216 | 217 | })( jQuery ); 218 | -------------------------------------------------------------------------------- /public/postit.html: -------------------------------------------------------------------------------- 1 |
2 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* Custom Styles */ 2 | 3 | .comparison h2{ 4 | font-size: 20px; 5 | margin-top: 0px; 6 | } 7 | 8 | img.smartlogo { 9 | float: right; 10 | margin-top: -0.4em; 11 | } 12 | 13 | body { 14 | padding-top: 60px; 15 | overflow-y: scroll; 16 | } 17 | 18 | .controls { 19 | margin-bottom: 1em; 20 | } 21 | #pasteHere { 22 | width: 100% 23 | } 24 | 25 | .scoreOneline { 26 | width: 90%; 27 | } 28 | 29 | .scorePoints { 30 | width: 10%; 31 | font-weight: bold; 32 | } 33 | 34 | .rubricDetail{} 35 | .comparison {margin-bottom: 0.5em;} 36 | .grade {margin-right: 10px;} 37 | 38 | .headerTable { 39 | display: inline-block; margin-bottom: 0.2em; 40 | } 41 | 42 | .scoreTable td.scoreOneline, .scoreTable td.scorePoints { 43 | cursor: pointer; 44 | } 45 | .pointer{ 46 | cursor: pointer; 47 | float: right; 48 | font-size: 14px; 49 | } 50 | 51 | .tweetButton{ 52 | float: left; 53 | margin-top: -.25em; 54 | } 55 | .pointer:last-child{ 56 | margin-left: 1em; 57 | } 58 | 59 | .spaceRight{ 60 | margin-right: 1em; 61 | } 62 | 63 | .fadedIn { 64 | opacity: 1; 65 | } 66 | 67 | .fadedOut { 68 | opacity: 0; 69 | } 70 | .detailRow{ 71 | padding-bottom: 1.5em; 72 | } 73 | 74 | table h2 {margin-bottom: 0em;} 75 | 76 | .histogram { 77 | float: right; 78 | text-align: center; 79 | font-style: italic; 80 | } 81 | 82 | 83 | .chart rect { 84 | fill: lightslategrey; 85 | shape-rendering: crispEdges; 86 | stroke: white; 87 | } 88 | .chart line { 89 | stroke: steelblue; 90 | } 91 | 92 | .chart text { 93 | color: blueblack; 94 | font-size: 8pt; 95 | } 96 | 97 | #overall{ 98 | vertical-align: middle; 99 | margin-right: .5em; 100 | font-family: 'Domine', serif; 101 | font-weight: 400; 102 | font-size: 4em; 103 | margin-top: 1em; 104 | margin-bottom: 1em; 105 | float: left; 106 | } 107 | 108 | #overall strong{ 109 | font-weight: 700; 110 | } 111 | 112 | #shareDocument { 113 | margin-top: -3em; 114 | margin-bottom: .7em; 115 | } 116 | 117 | #collapseButtons { 118 | margin-bottom: 2em; 119 | } 120 | 121 | #collapseButtons a { 122 | float: left; 123 | margin-right: .25em; 124 | } 125 | 126 | .hero-unit.beautiful, .well.helpSmart { 127 | background: white; 128 | margin-top: 5em; 129 | } 130 | 131 | .bestPractice { 132 | margin-bottom: 2em; 133 | } 134 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'); 2 | var app = config.app; 3 | 4 | app.get('/', function(req,res){ 5 | res.redirect("/static/ccdaScorecard"); 6 | }); 7 | 8 | var Scorecard = require('./controllers/ccda_scorecard'); 9 | var Examples = require('./controllers/examples'); 10 | 11 | app.post('/v1/post-to-ui?', Scorecard.postToUi); 12 | app.post('/v1/ccda-scorecard/request/?', Scorecard.gradeRequest); 13 | app.get('/v1/ccda-scorecard/rubrics/?', Scorecard.rubricAll); 14 | app.get('/v1/ccda-scorecard/rubrics/:rid', Scorecard.rubricOne); 15 | app.get('/v1/ccda-scorecard/stats/?', Scorecard.statsAll); 16 | app.get('/v1/ccda-scorecard/stats/:rid', Scorecard.statsOne); 17 | app.get('/v1/examples/?', Examples.list) 18 | app.get('/v1/examples/:id', Examples.fetch) 19 | -------------------------------------------------------------------------------- /rubrics/codes-01-exist.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){ }; 4 | 5 | var umlsSystems = [ 6 | "2.16.840.1.113883.6.96", // snomed ct 7 | "2.16.840.1.113883.6.1", // loinc 8 | "2.16.840.1.113883.6.88" // rxnorm 9 | ]; 10 | 11 | var exceptions = ["http://purl.bioontology.org/ontology/LNC/30954-2"]; // LOINC results section. Or so people like to call it 12 | 13 | rubric.prototype.report = function(done){ 14 | var ccda = this.manager.ccda; 15 | var vocab = this.manager.vocab; 16 | 17 | var codes = vocab.sourceCodes.filter(function(c){ 18 | return (umlsSystems.indexOf(c.codeSystem) !== -1); 19 | }); 20 | 21 | var hits = []; 22 | var misses = []; 23 | 24 | codes.forEach(function(c){ 25 | vocab.lookup(c) ? hits.push(c) : misses.push(c); 26 | }); 27 | 28 | var points = (hits.length === codes.length) ? codes.length : 0; 29 | 30 | var report = common.report(rubric, points, codes.length, { 31 | hits: hits, 32 | misses: misses 33 | }); 34 | 35 | done(null, report); 36 | }; 37 | -------------------------------------------------------------------------------- /rubrics/codes-01-exist.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["General", "Codes"], 4 | "description": "SNOMED CT, LOINC, and RxNorm codes validate against UMLS", 5 | "detail": "Codes that claim to be from SNOMED CT, LOINC, and RxNorm should be present in UMLS 2014AA.", 6 | "maxPoints": 5, 7 | "points": { 8 | "5": "All codes that claim to be from UMLS are found there", 9 | "0": "Some codes that claim to be from UMLS are not found there" 10 | }, 11 | "doesNotApply": "No codes in document" 12 | } 13 | -------------------------------------------------------------------------------- /rubrics/codes-01-exist.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | codes weren't found in UMLS 2014AA

6 | 7 | 8 | 9 | 10 | 11 | 12 | <% misses.forEach(function(miss){ %> 13 | 14 | 17 | 20 | 21 | 22 | <% }) %> 23 |
Your codeWhat now?
15 | <%= miss.codeSystemName %>:<%= miss.code %> 16 | 18 | "<%= miss.displayName %>" 19 | Check mapping
24 | <% } %> 25 | -------------------------------------------------------------------------------- /rubrics/codes-02-displaynames-correct.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){}; 4 | 5 | var umlsSystems = [ 6 | "2.16.840.1.113883.6.96", // snomed ct 7 | "2.16.840.1.113883.6.1", // loinc 8 | "2.16.840.1.113883.6.88" // rxnorm 9 | ]; 10 | 11 | var exceptions = ["http://purl.bioontology.org/ontology/LNC/30954-2"]; // LOINC results section. Or so people like to call it 12 | 13 | rubric.prototype.report = function(done){ 14 | var ccda = this.manager.ccda; 15 | var vocab = this.manager.vocab; 16 | 17 | var codes = vocab.sourceCodes.filter(function(c){ 18 | return (umlsSystems.indexOf(c.codeSystem) !== -1); 19 | }); 20 | 21 | var hits = []; 22 | var misses = []; 23 | 24 | codes.forEach(function(c){ 25 | var umls = vocab.lookup(c); 26 | 27 | if (!umls || !c.displayName || !c.codeSystemName) { 28 | return; // TODO: check for this error condition in a separate rubric 29 | } 30 | 31 | if (exceptions.indexOf(umls._id) !== -1){ 32 | return; 33 | } 34 | 35 | tokens = c.displayName.split(/[^A-z\-\/]+/) 36 | .filter(function(t){return t.length > 1;}) 37 | .map(function(t){ 38 | return t.toLowerCase(); 39 | }); 40 | 41 | 42 | unmatchedTokens = tokens.filter(function(t){ 43 | return (umls.conceptNameTokens.indexOf(t) === -1); 44 | }); 45 | 46 | 47 | if (unmatchedTokens.length * 1.0 / tokens.length > .5){ 48 | c.normalized = umls; 49 | console.log(tokens, tokens.length, unmatchedTokens, unmatchedTokens.lenth, "UNMATCHED"); 50 | return misses.push(c); 51 | } 52 | 53 | return hits.push(c); 54 | }); 55 | 56 | var numerator = hits.length; 57 | var denominator = misses.length + numerator; 58 | 59 | var report = common.report(rubric, numerator, denominator, { 60 | hits: hits, 61 | misses: misses 62 | }); 63 | 64 | done(null, report); 65 | }; 66 | -------------------------------------------------------------------------------- /rubrics/codes-02-displaynames-correct.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["General", "Codes"], 4 | "description": "SNOMED CT, LOINC, and RxNorm codes match their displayName", 5 | "detail": "Codes in a C-CDA should assign a valid @displayName that reflects the meaning of the underlying concept. A best practice is to use preferred labels from UMLS.", 6 | "maxPoints": 3, 7 | "points": { 8 | "3": "> 80% of codes have appropriate @displayName", 9 | "2": "> 50% of codes have appropriate @displayName", 10 | "1": "At least one medication has appropriate @displayName", 11 | "0": "No codes have appropriate @displayName" 12 | }, 13 | "doesNotApply": "No codes in document" 14 | } 15 | -------------------------------------------------------------------------------- /rubrics/codes-02-displaynames-correct.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | codes didn't match their displayName

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% misses.forEach(function(miss){ %> 14 | 15 | 18 | 21 | 24 | 27 | 28 | <% }) %> 29 |
Your codePreferred termWhat now?
16 | <%= miss.codeSystemName %>:<%= miss.code %> 17 | 19 | "<%= miss.displayName %>" 20 | 22 | "<%= miss.normalized.conceptName %>" 23 | 25 | See <%= miss.normalized._id %> 26 |
30 | <% } %> 31 | -------------------------------------------------------------------------------- /rubrics/dates-01-precision.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | var rubric = module.exports = function(){}; 3 | 4 | rubric.prototype.test = function datePrecision(d){ 5 | if (d.match(/\.000/) || d.match(/^..+0000/)){ 6 | this.fail(d, "Confirm this is the intended level of precision"); 7 | } 8 | } 9 | 10 | var times = "//h:birthTime/@value | " + 11 | "//h:effectiveTime/@value | " + 12 | "//h:effectiveTime/h:low/@value | "+ 13 | "//h:effectiveTime/h:high/@value"; 14 | 15 | rubric.prototype.inputs = function(){ 16 | return common.xpath(this.manager.ccda, times).map(function(t){ 17 | return t.value(); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /rubrics/dates-01-precision.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["c-cda", "smart"], 3 | "category": ["General", "Dates"], 4 | "description": "Document uses sensible datetime precision", 5 | "detail": "C-CDA's datetimes should use ISO8601 strings to express appropriate precision.", 6 | "maxPoints": 1, 7 | "points": { 8 | "1": "Everywhere", 9 | "0": "Some implausible datetime precisions" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /rubrics/disabled/nulliness.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){ }; 4 | 5 | rubric.prototype.report = function(done){ 6 | var numCodes = common.xpath(this.manager.ccda, "//h:code").length; 7 | var numNulls = common.xpath(this.manager.ccda, "//@nullFlavor").length; 8 | 9 | var numerator = numCodes; 10 | var denominator = numCodes + numNulls; 11 | var report = common.report(rubric, numerator, denominator); 12 | 13 | done(null, report); 14 | }; 15 | -------------------------------------------------------------------------------- /rubrics/disabled/nulliness.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["General", "Codes"], 4 | "description": "Codes are not null", 5 | "detail": "High-quality data are coded. Codes should not be 'nullFlavor'", 6 | "maxPoints": 3, 7 | "points": { 8 | "3": "> 90% code:null ratio", 9 | "2": "> 50% code:null ratio", 10 | "1": "At least 1 code used", 11 | "0": "No codes anywhere (unlikely)" 12 | }, 13 | "doesNotApply": "No codes or nulls in documents" 14 | } 15 | -------------------------------------------------------------------------------- /rubrics/labcodes.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){}; 4 | 5 | rubric.prototype.report = function(done){ 6 | var ccda = this.manager.ccda; 7 | var vocab = this.manager.vocab; 8 | 9 | var codes = common.valueSetMembership(ccda, labCodeXpath, recommendedValueSets, vocab); 10 | 11 | var numerator = codes.inSet.length; 12 | var denominator = codes.notInSet.length + numerator; 13 | 14 | var report = common.report( 15 | rubric, 16 | numerator, 17 | denominator, 18 | {hits: codes.inSet, misses: codes.notInSet} 19 | ); 20 | 21 | done(null, report); 22 | }; 23 | 24 | var labCodeXpath = "( \ 25 | //h:templateId[@root='2.16.840.1.113883.10.20.22.2.3']/.. | \ 26 | //h:templateId[@root='2.16.840.1.113883.10.20.22.2.3.1']/.. \ 27 | )\ 28 | //h:templateId[@root='2.16.840.1.113883.10.20.22.4.2']/../\ 29 | h:code"; 30 | 31 | // internal Value Set OID surrogate, since 32 | // there's no official OID for this top-2k extract 33 | var recommendedValueSets = [ 34 | // Unofficial SMART recommendation 35 | "LOINC Top 2000" 36 | ]; 37 | -------------------------------------------------------------------------------- /rubrics/labcodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["c-cda", "smart"], 3 | "category": ["Lab Results", "Codes"], 4 | "description": "Lab Results coded with LOINC's top 2K codes", 5 | "detail": "Lab results should be coded using LOINC. In pratice LOINC is huge, but 2000 codes cover 98% of real-world usage. This means that most results should be covered by the 2000+ most common LOINC codes published by Regenstrief.", 6 | "maxPoints": 3, 7 | "points": { 8 | "3": "> 80% of lab results have a top-2K LOINC code", 9 | "2": "> 50% of lab results have a top-2K LOINC code", 10 | "1": "At least one lab result has a top-2K LOINC code", 11 | "0": "No lab results have a top-2K LOINC code" 12 | }, 13 | "doesNotApply": "No lab results in document" 14 | } 15 | -------------------------------------------------------------------------------- /rubrics/labcodes.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | lab result codes weren't in the recommended value set 6 |

7 | 8 |

9 | Note: This may be normal, if this C-CDA document includes unusual labs for 10 | which no common LOINC code exists. But look through the un-matched codes 11 | below to make sure you don't have a mapping error in your export pipeline. 12 |

13 | 14 |

Recommended value set: 15 |

22 |

23 | 24 | <% include partials/codeTable%> 25 | 26 | <% } %> 27 | -------------------------------------------------------------------------------- /rubrics/medcodes.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){}; 4 | 5 | rubric.prototype.report = function(done){ 6 | var ccda = this.manager.ccda; 7 | var vocab = this.manager.vocab; 8 | 9 | var codes = common.valueSetMembership(ccda, medCodeXpath, recommendedValueSets, vocab); 10 | 11 | var numerator = codes.inSet.length; 12 | var denominator = codes.notInSet.length + numerator; 13 | 14 | console.log(codes.notInSet, "missed meds"); 15 | 16 | var report = common.report(rubric, numerator, denominator, { 17 | hits: codes.inSet, 18 | misses: codes.notInSet 19 | }); 20 | 21 | done(null, report); 22 | }; 23 | 24 | var medCodeXpath = "//h:templateId[@root='2.16.840.1.113883.10.20.22.2.1.1']/../" + 25 | "h:entry/h:substanceAdministration/"+ 26 | "h:templateId[@root='2.16.840.1.113883.10.20.22.4.16']/../" + 27 | "h:consumable/h:manufacturedProduct/" + 28 | "h:manufacturedMaterial/h:code"; 29 | 30 | var recommendedValueSets = [ 31 | // Unofficial SMART recommendation 32 | "RxNorm Generic Clinical Drug", 33 | "RxNorm Branded Clinical Drug" 34 | ]; 35 | -------------------------------------------------------------------------------- /rubrics/medcodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["c-cda", "smart"], 3 | "category": ["Medications", "Codes"], 4 | "description": "Medications coded with RxNorm SCD, SBD, GPCK, or BPCPK codes", 5 | "detail": "C-CDA medication lists should contain medications coded as RxNorm Semantic Clinical Drugs, Semantic Branded Drugs, and packs. This means prescribable products on the level of 'loratadine 10mg oral tablet'.)", 6 | "maxPoints": 3, 7 | "points": { 8 | "3": "> 80% of meds have an RxNorm SCD code", 9 | "2": "> 50% of meds have an RxNorm SCD code", 10 | "1": "At least one medication has an RxNorm SCD code", 11 | "0": "No meds have an RxNorm SCD code" 12 | }, 13 | "doesNotApply": "No medications in document" 14 | } 15 | -------------------------------------------------------------------------------- /rubrics/medcodes.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | medication codes weren't in the recommended value sets 6 |

7 | 8 | 9 |

10 | Recommended value sets: 11 |

28 |

29 | 30 | <% include partials/codeTable%> 31 | 32 | <%}%> 33 | -------------------------------------------------------------------------------- /rubrics/partials/codeTable.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% 9 | misses.forEach(function(miss){ 10 | var issue = ""; 11 | 12 | if (miss.nullFlavor) {issue = "nullFlavor='"+miss.nullFlavor+"' was used"} 13 | else if (!miss.normalized) {issue = "Code not found in UMLS 2014AA"} 14 | else if (!miss.normalized.active) {issue = "Obsolete"} 15 | else {issue = "Not in value set"} 16 | %> 17 | 18 | 26 | <% if (miss.displayName) { %> 27 | 28 | <% } else { %> 29 | 30 | <% } %> 31 | 32 | 38 | 39 | <% 40 | }); 41 | %> 42 |
Your codeThe issueWhat now?
19 | <% if (miss.codeSystemName && miss.code) { %> 20 | <%= miss.codeSystemName %>:<%= miss.code %> 21 | <%= miss.nullFlavor ? " nullFlavor='"+miss.nullFlavor+"'" : "" %> 22 | <% } else { %> 23 | <%= miss.node.toString() %> 24 | <% } %> 25 | "<%= miss.displayName %>"(No title)<%= issue %> <% if (miss.normalized && !miss.nullFlavor){%> 33 | See <%= miss.normalized._id %> 34 | <%} else {%> 35 | Check mapping 36 | <%}%> 37 |
43 | -------------------------------------------------------------------------------- /rubrics/partials/mistakes.ejs: -------------------------------------------------------------------------------- 1 | <% if ( typeof mistakes !== "undefined" && mistakes.length > 0) { 2 | 3 | var first = mistakes.length > 10 ? 10 : mistakes.length; 4 | %> 5 | 6 |

7 | <%= mistakes.length %> 8 | issue<%=mistakes.length > 1 ? "s":""%>. 9 | <% if(first < mistakes.length){ %> 10 | Showing first ten... 11 | <% } %> 12 |

13 | 14 | 15 | 16 | <% 17 | mistakes.slice(0,first).forEach(function(miss){ 18 | %> 19 | 20 | <% miss.forEach(function(m){ %> 21 | 24 | <%})%> 25 | 26 | <% }) %> 27 |
22 | <%= m %> 23 |
28 | <% } %> 29 | -------------------------------------------------------------------------------- /rubrics/problemcodes.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){}; 4 | 5 | rubric.prototype.report = function(done){ 6 | 7 | var ccda = this.manager.ccda; 8 | var vocab = this.manager.vocab; 9 | 10 | var codes = common.valueSetMembership(ccda, problemCodes, recommendedValueSets, vocab); 11 | 12 | var numerator = codes.inSet.length; 13 | var denominator = codes.notInSet.length + numerator; 14 | 15 | var report = common.report(rubric, numerator, denominator, { 16 | hits: codes.notInSet, 17 | misses: codes.notInSet 18 | }); 19 | 20 | done(null, report); 21 | }; 22 | 23 | var templateIds = { 24 | concern: "2.16.840.1.113883.10.20.22.4.3", 25 | problem: "2.16.840.1.113883.10.20.22.4.4" 26 | } 27 | 28 | var problemCodes = "//h:templateId[@root='"+templateIds.concern+"']/.."+ 29 | "//h:templateId[@root='"+templateIds.problem+"']/.." + 30 | "/h:value"; 31 | 32 | var recommendedValueSets = [ 33 | // official C-CDA recommendation, but ~2009 (16k terms) 34 | "SNOMED VA/KP Problem List", 35 | 36 | // updated with each SNOMED CT release (5k terms) 37 | "SNOMED CORE Problem List" 38 | ]; 39 | -------------------------------------------------------------------------------- /rubrics/problemcodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["c-cda", "smart"], 3 | "category": ["Problems", "Codes"], 4 | "description": "Problems coded with HITSP's 16K SNOMED subset", 5 | "detail": "Each problem in the problem list should be coded with a SNOMED code from the HITSP Problem List valueset (OID 2.16.840.1.113883.3.88.12.3221.7.4).", 6 | "maxPoints": 3, 7 | "points": { 8 | "3": "> 80% of problems have a HITSP 16K SNOMED code", 9 | "2": "> 50% of problems have a HITSP 16K SNOMED code", 10 | "1": "At least one problem has a HITSP 16K SNOMED code", 11 | "0": "No problems have a HITSP 16K SNOMED code" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rubrics/problemcodes.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | problem codes weren't in the recommended value sets 6 |

7 | 8 |

9 | Note: This may be normal, if this C-CDA document includes unusual problems for 10 | which no appropriate SNOMED code exists. But look through the un-matched codes 11 | below to make sure you don't have a mapping error in your export pipeline. 12 |

13 | 14 |

15 | Recommended value sets: 16 |

28 | 29 |

30 | 31 | 32 | <% include partials/codeTable%> 33 | <% } %> 34 | -------------------------------------------------------------------------------- /rubrics/problemstatus.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | var xpath = common.xpath; 3 | 4 | var rubric = module.exports = function(){}; 5 | 6 | rubric.prototype.report = function(done){ 7 | var ccda = this.manager.ccda; 8 | var concerns = xpath(ccda, xpaths.concerns); 9 | 10 | var numerator = 0; 11 | var denominator = concerns.length; 12 | 13 | concerns.forEach(function(concern){ 14 | 15 | var concernStatus = concernStatusMap[xpath(concern, xpaths.concernStatusText)]; 16 | var concernEndDate = xpath(concern, xpaths.concernDate).toString(); 17 | 18 | // active conern with an end date == mistake! 19 | if (concernStatus === true && concernEndDate) { 20 | return; 21 | } 22 | 23 | var problemStatus = xpath(concern, xpaths.problemStatus).map(function(s){ 24 | return problemStatusMap[s.value()]; 25 | }); 26 | 27 | if (problemStatus.length === 0) { 28 | return numerator++; 29 | } 30 | 31 | if (problemStatus.length !== 1){ 32 | return; 33 | } 34 | problemStatus = problemStatus[0]; 35 | 36 | if (concernStatus === undefined || problemStatus === undefined){ 37 | return; 38 | } 39 | 40 | if (problemStatus !== concernStatus){ 41 | return; 42 | } 43 | 44 | return numerator++; 45 | }); 46 | 47 | var report = common.report(rubric, 48 | numerator===denominator? denominator:0, denominator); 49 | 50 | done(null, report); 51 | }; 52 | 53 | var templateIds = { 54 | concern: "2.16.840.1.113883.10.20.22.4.3", 55 | problem: "2.16.840.1.113883.10.20.22.4.4", 56 | problemStatus: "2.16.840.1.113883.10.20.22.4.6" 57 | } 58 | 59 | var xpaths = { 60 | concerns: "//h:templateId[@root='"+templateIds.concern+"']/..", 61 | concernStatusText: "string(h:statusCode/@code)", 62 | problemStatus: ".//h:templateId[@root='"+templateIds.problem+"']/.." + 63 | "//h:templateId[@root='"+templateIds.problemStatus+"']/.." + 64 | "/h:value/@displayName", 65 | concernDate: "./h:effectiveTime/h:high/@value" 66 | } 67 | 68 | var problemStatusMap = { 69 | "Active" : true, 70 | "Inactive": false, 71 | "Resolved": false 72 | }; 73 | 74 | var concernStatusMap = { 75 | "active" : true, 76 | "completed" : false 77 | }; 78 | -------------------------------------------------------------------------------- /rubrics/problemstatus.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["Problems"], 4 | "description": "Problem statuses are internally consistent", 5 | "detail": "Each concern act should contain exactly one problem. If there is a status attached to the concern act as well as the problem, these should not contradict. A concern status of 'completed' is compatible with a problem status of 'Resolved' or 'Inactive'. A concern status of 'active' is compatible with a problem status of 'Active'.", 6 | "maxPoints": 3, 7 | "points": { 8 | "3": "Each problem has consistent active/complete status", 9 | "0": "Some problems have inconsistent active/complete status" 10 | }, 11 | "doesNotApply": "No problems document" 12 | } 13 | -------------------------------------------------------------------------------- /rubrics/shared/codes.js: -------------------------------------------------------------------------------- 1 | var unitMap = { 2 | '8310-5': {units: ['Cel', '[degF]' ]}, 3 | '8462-4': {units: ['mm[Hg]']}, 4 | '8480-6': {units: ['mm[Hg]']}, 5 | '8287-5': {units: ['cm', '[in_i]', '[in_us]']}, 6 | '8867-4': {units: ['/min', '{beats}/min']}, 7 | '8302-2': {units: ['cm', '[in_i]', '[in_us]']}, 8 | '8306-3': {units: ['cm', '[in_i]', '[in_us]']}, 9 | '2710-2': {units: ['%']}, 10 | '9279-1': {units: ['/min', '{breaths}/min']}, 11 | '3141-9': {units: ['g', 'kg', '[lb_av]', '[oz_av]']}, 12 | '39156-5': {units: ['kg/m2']}, 13 | '3140-1': {units: ['m2']} 14 | }; 15 | 16 | 17 | module.exports = { 18 | unitMap: unitMap 19 | } 20 | -------------------------------------------------------------------------------- /rubrics/shared/templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "old": [ 4 | "2.16.840.1.113883.10.20.1.8", 5 | "2.16.840.1.113883.3.88.11.83.112", 6 | "1.3.6.1.4.1.19376.1.5.3.1.3.19" 7 | ], 8 | "new": [ 9 | "2.16.840.1.113883.10.20.22.2.1", 10 | "2.16.840.1.113883.10.20.22.2.1.1" 11 | ] 12 | }, 13 | 14 | { 15 | "old": [ 16 | "2.16.840.1.113883.10.20.16.2.2", 17 | "2.16.840.1.113883.3.88.11.83.114", 18 | "1.3.6.1.4.1.19376.1.5.3.1.3.22" 19 | ], 20 | "new": [ 21 | "2.16.840.1.113883.10.20.22.2.11.1", 22 | "2.16.840.1.113883.10.20.22.2.11" 23 | ] 24 | }, 25 | 26 | { 27 | "old": [ 28 | "2.16.840.1.113883.10.20.18.2.8", 29 | "2.16.840.1.113883.3.88.11.83.115", 30 | "1.3.6.1.4.1.19376.1.5.3.1.3.21" 31 | ], 32 | "new": [ 33 | "2.16.840.1.113883.10.20.22.2.38" 34 | ] 35 | }, 36 | 37 | { 38 | "old": [ 39 | "2.16.840.1.113883.10.20.1.6", 40 | "2.16.840.1.113883.3.88.11.83.117", 41 | "1.3.6.1.4.1.19376.1.5.3.1.3.23" 42 | ], 43 | "new": [ 44 | "2.16.840.1.113883.10.20.22.2.2" 45 | ] 46 | }, 47 | 48 | { 49 | "old": [ 50 | "2.16.840.1.113883.10.20.1.2", 51 | "2.16.840.1.113883.3.88.11.83.102", 52 | "1.3.6.1.4.1.19376.1.5.3.1.3.13" 53 | ], 54 | "new": [ 55 | "2.16.840.1.113883.10.20.22.2.6.1", 56 | "2.16.840.1.113883.10.20.22.2.6" 57 | ] 58 | }, 59 | 60 | { 61 | "old": [ 62 | "2.16.840.1.113883.10.20.1.11", 63 | "2.16.840.1.113883.3.88.11.83.103", 64 | "1.3.6.1.4.1.19376.1.5.3.1.3.6" 65 | ], 66 | "new": [ 67 | "2.16.840.1.113883.10.20.22.2.5.1", 68 | "2.16.840.1.113883.10.20.22.2.5" 69 | ] 70 | }, 71 | 72 | { 73 | "old": [ 74 | "2.16.840.1.113883.10.20.2.9", 75 | "2.16.840.1.113883.3.88.11.83.104", 76 | "1.3.6.1.4.1.19376.1.5.3.1.3.8" 77 | ], 78 | "new": [ 79 | "2.16.840.1.113883.10.20.22.2.20" 80 | ] 81 | }, 82 | 83 | { 84 | "old": [ 85 | "2.16.840.1.113883.10.20.16.2.1", 86 | "2.16.840.1.113883.3.88.11.83.111", 87 | "1.3.6.1.4.1.19376.1.5.3.1.3.7" 88 | ], 89 | "new": [ 90 | "2.16.840.1.113883.10.20.22.2.24" 91 | ] 92 | }, 93 | 94 | { 95 | "old": [ 96 | "2.16.840.1.113883.10.20.7.1", 97 | "2.16.840.1.113883.3.88.11.83.129" 98 | ], 99 | "new": [ 100 | "2.16.840.1.113883.10.20.22.2.34" 101 | ] 102 | }, 103 | 104 | { 105 | "old": [ 106 | "2.16.840.1.113883.10.20.7.2", 107 | "2.16.840.1.113883.3.88.11.83.130" 108 | ], 109 | "new": [ 110 | "2.16.840.1.113883.10.20.22.2.35" 111 | ] 112 | }, 113 | 114 | { 115 | "old": [ 116 | "2.16.840.1.113883.10.20.2.8", 117 | "2.16.840.1.113883.10.20.18.2.16", 118 | "2.16.840.1.113883.3.88.11.83.105", 119 | "1.3.6.1.4.1.19376.1.5.3.1.1.13.2.1" 120 | ], 121 | "new": [ 122 | "1.3.6.1.4.1.19376.1.5.3.1.1.13.2.1", 123 | "2.16.840.1.113883.10.20.22.2.12", 124 | "2.16.840.1.113883.10.20.22.2.13" 125 | ] 126 | }, 127 | 128 | { 129 | "old": [ 130 | "2.16.840.1.113883.10.20.4.8", 131 | "2.16.840.1.113883.3.88.11.83.106", 132 | "1.3.6.1.4.1.19376.1.5.3.1.3.1", 133 | "1.3.6.1.4.1.19376.1.5.3.1.3.2" 134 | ], 135 | "new": [ 136 | "1.3.6.1.4.1.19376.1.5.3.1.3.1" 137 | ] 138 | }, 139 | 140 | { 141 | "old": [ 142 | "1.3.6.1.4.1.19376.1.5.3.1.3.4", 143 | "2.16.840.1.113883.3.88.11.83.107", 144 | "1.3.6.1.4.1.19376.1.5.3.1.3.4" 145 | ], 146 | "new": [ 147 | "1.3.6.1.4.1.19376.1.5.3.1.3.4" 148 | ] 149 | }, 150 | 151 | { 152 | "old": [ 153 | "2.16.840.1.113883.10.20.18.2.5" 154 | ], 155 | "new": [ 156 | "2.16.840.1.113883.10.20.22.2.39" 157 | ] 158 | }, 159 | 160 | { 161 | "old": [ 162 | "2.16.840.1.113883.10.20.1.12", 163 | "2.16.840.1.113883.10.20.18.2.18", 164 | "2.16.840.1.113883.3.88.11.83.108", 165 | "1.3.6.1.4.1.19376.1.5.3.1.3.12" 166 | ], 167 | "new": [ 168 | "2.16.840.1.113883.10.20.22.2.7" 169 | ] 170 | }, 171 | 172 | { 173 | "old": [ 174 | "2.16.840.1.113883.10.20.18.2.1" 175 | ], 176 | "new": [ 177 | "2.16.840.1.113883.10.20.22.2.29" 178 | ] 179 | }, 180 | 181 | { 182 | "old": [ 183 | "2.16.840.1.113883.10.20.18.2.2" 184 | ], 185 | "new": [ 186 | "2.16.840.1.113883.10.20.22.2.27" 187 | ] 188 | }, 189 | 190 | { 191 | "old": [ 192 | "2.16.840.1.113883.10.20.18.2.3" 193 | ], 194 | "new": [ 195 | "2.16.840.1.113883.10.20.22.2.36" 196 | ] 197 | }, 198 | 199 | { 200 | "old": [ 201 | "2.16.840.1.113883.10.20.18.2.4", 202 | "2.16.840.1.113883.10.20.7.10" 203 | ], 204 | "new": [ 205 | "2.16.840.1.113883.10.20.22.2.37" 206 | ] 207 | }, 208 | 209 | { 210 | "old": [ 211 | "2.16.840.1.113883.10.20.18.2.7", 212 | "2.16.840.1.113883.10.20.7.5" 213 | ], 214 | "new": [ 215 | "2.16.840.1.113883.10.20.22.2.25" 216 | ] 217 | }, 218 | 219 | { 220 | "old": [ 221 | "2.16.840.1.113883.10.20.18.2.15" 222 | ], 223 | "new": [ 224 | "2.16.840.1.113883.10.20.22.2.28" 225 | ] 226 | }, 227 | { 228 | "old": [ 229 | "2.16.840.1.113883.10.20.18.2.11" 230 | ], 231 | "new": [ 232 | "2.16.840.1.113883.10.20.22.2.40" 233 | ] 234 | }, 235 | 236 | { 237 | "old": [ 238 | "2.16.840.1.113883.10.20.18.2.6" 239 | ], 240 | "new": [ 241 | "2.16.840.1.113883.10.20.22.2.30" 242 | ] 243 | }, 244 | 245 | { 246 | "old": [ 247 | "2.16.840.1.113883.10.20.18.2.10" 248 | ], 249 | "new": [ 250 | "2.16.840.1.113883.10.20.22.2.31" 251 | ] 252 | }, 253 | 254 | { 255 | "old": [ 256 | "2.16.840.1.113883.10.20.2.7", 257 | "2.16.840.1.113883.10.20.18.2.13", 258 | "1.3.6.1.4.1.19376.1.5.3.1.1.13.2.4", 259 | "1.3.6.1.4.1.19376.1.5.3.1.1.13.2.4" 260 | ], 261 | "new": [ 262 | "2.16.840.1.113883.10.20.22.2.8" 263 | ] 264 | }, 265 | 266 | { 267 | "old": [ 268 | "2.16.840.1.113883.10.20.2.7", 269 | "2.16.840.1.113883.10.20.18.2.14", 270 | "1.3.6.1.4.1.19376.1.5.3.1.1.13.2.5" 271 | ], 272 | "new": [ 273 | "2.16.840.1.113883.10.20.22.2.9" 274 | ] 275 | }, 276 | 277 | { 278 | "old": [ 279 | "2.16.840.1.113883.10.20.2.7", 280 | "2.16.840.1.113883.10.20.1.10", 281 | "2.16.840.1.113883.3.88.11.83.124", 282 | "1.3.6.1.4.1.19376.1.5.3.1.3.31" 283 | ], 284 | "new": [ 285 | "2.16.840.1.113883.10.20.22.2.10" 286 | ] 287 | }, 288 | 289 | { 290 | "old": [ 291 | "2.16.840.1.113883.10.20.1.5", 292 | "2.16.840.1.113883.3.88.11.83.109", 293 | "1.3.6.1.4.1.19376.1.5.3.1.3.17" 294 | ], 295 | "new": [ 296 | "2.16.840.1.113883.10.20.22.2.14" 297 | ] 298 | }, 299 | 300 | { 301 | "old": [ 302 | "2.16.840.1.113883.10.20.1.14", 303 | "2.16.840.1.113883.3.88.11.83.122", 304 | "1.3.6.1.4.1.19376.1.5.3.1.3.28" 305 | ], 306 | "new": [ 307 | "2.16.840.1.113883.10.20.22.2.3.1", 308 | "2.16.840.1.113883.10.20.22.2.3" 309 | ] 310 | }, 311 | 312 | { 313 | "old": [ 314 | "2.16.840.1.113883.10.20.1.16", 315 | "2.16.840.1.113883.10.20.2.4", 316 | "2.16.840.1.113883.3.88.11.83.119", 317 | "1.3.6.1.4.1.19376.1.5.3.1.3.25" 318 | ], 319 | "new": [ 320 | "2.16.840.1.113883.10.20.22.2.4.1", 321 | "2.16.840.1.113883.10.20.22.2.4" 322 | ] 323 | }, 324 | 325 | { 326 | "old": [ 327 | "2.16.840.1.113883.10.20.1.9", 328 | "2.16.840.1.113883.3.88.11.83.101.1", 329 | "1.3.6.1.4.1.19376.1.5.3.1.1.5.3.7" 330 | ], 331 | "new": [ 332 | "2.16.840.1.113883.10.20.22.2.18" 333 | ] 334 | }, 335 | 336 | { 337 | "old": [ 338 | "2.16.840.1.113883.10.20.1.1", 339 | "2.16.840.1.113883.3.88.11.83.116", 340 | "1.3.6.1.4.1.19376.1.5.3.1.3.34", 341 | "1.3.6.1.4.1.19376.1.5.3.1.3.35" 342 | ], 343 | "new": [ 344 | "2.16.840.1.113883.10.20.22.2.21" 345 | ] 346 | }, 347 | 348 | { 349 | "old": [ 350 | "2.16.840.1.113883.10.20.2.10", 351 | "2.16.840.1.113883.3.88.11.83.118", 352 | "1.3.6.1.4.1.19376.1.5.3.1.3.24", 353 | "1..3.6.1.4.1.19376.1.5.3.1.1.9.15" 354 | ], 355 | "new": [ 356 | "2.16.840.1.113883.10.20.2.10" 357 | ] 358 | }, 359 | 360 | { 361 | "old": [ 362 | "2.16.840.1.113883.10.20.4.10", 363 | "2.16.840.1.113883.3.88.11.83.120", 364 | "1.3.6.1.4.1.19376.1.5.3.1.3.18" 365 | ], 366 | "new": [ 367 | "1.3.6.1.4.1.19376.1.5.3.1.3.18" 368 | ] 369 | }, 370 | 371 | { 372 | "old": [ 373 | "1.3.6.1.4.1.19376.1.5.3.1.3.5", 374 | "2.16.840.1.113883.3.88.11.83.121" 375 | ], 376 | "new": [ 377 | "1.3.6.1.4.1.19376.1.5.3.1.3.5" 378 | ] 379 | }, 380 | 381 | { 382 | "old": [ 383 | "2.16.840.1.113883.10.20.1.4", 384 | "2.16.840.1.113883.10.20.18.2.17", 385 | "2.16.840.1.113883.3.88.11.83.125", 386 | "1.3.6.1.4.1.19376.1.5.3.1.3.14", 387 | "1.3.6.1.4.1.19376.1.5.3.1.3.15" 388 | ], 389 | "new": [ 390 | "2.16.840.1.113883.10.20.22.2.15" 391 | ] 392 | }, 393 | 394 | { 395 | "old": [ 396 | "2.16.840.1.113883.10.20.1.15", 397 | "2.16.840.1.113883.3.88.11.83.126", 398 | "1.3.6.1.4.1.19376.1.5.3.1.3.16" 399 | ], 400 | "new": [ 401 | "2.16.840.1.113883.10.20.22.2.17" 402 | ] 403 | }, 404 | 405 | { 406 | "old": [ 407 | "2.16.840.1.113883.10.20.1.3", 408 | "2.16.840.1.113883.3.88.11.83.127", 409 | "1.3.6.1.4.1.19376.1.5.3.1.1.5.3.3" 410 | ], 411 | "new": [ 412 | "2.16.840.1.113883.10.20.22.2.22" 413 | ] 414 | }, 415 | 416 | { 417 | "old": [ 418 | "2.16.840.1.113883.10.20.1.7", 419 | "2.16.840.1.113883.3.88.11.83.128", 420 | "1.3.6.1.4.1.19376.1.5.3.1.1.5.3.5" 421 | ], 422 | "new": [ 423 | "2.16.840.1.113883.10.20.22.2.23" 424 | ] 425 | }, 426 | 427 | { 428 | "old": [ 429 | "2.16.840.1.113883.10.20.16.2.3" 430 | ], 431 | "new": [ 432 | "2.16.840.1.113883.10.20.22.2.16" 433 | ] 434 | }, 435 | 436 | { 437 | "old": [ 438 | "2.16.840.1.113883.10.20.1.32" 439 | ], 440 | "new": [ 441 | "2.16.840.1.113883.10.20.22.4.1" 442 | ] 443 | }, 444 | 445 | { 446 | "old": [ 447 | "2.16.840.1.113883.10.20.1.31", 448 | "2.16.840.1.113883.3.88.11.83.15.1" 449 | ], 450 | "new": [ 451 | "2.16.840.1.113883.10.20.22.4.2" 452 | ] 453 | }, 454 | 455 | { 456 | "old": [ 457 | "2.16.840.1.113883.10.20.1.27", 458 | "1.3.6.1.4.1.19376.1.5.3.1.4.5.1", 459 | "1.3.6.1.4.1.19376.1.5.3.1.4.5.2", 460 | "2.16.840.1.113883.3.88.11.83.7" 461 | ], 462 | "new": [ 463 | "2.16.840.1.113883.10.20.22.4.3" 464 | ] 465 | }, 466 | 467 | { 468 | "old": [ 469 | "2.16.840.1.113883.10.20.1.28", 470 | "1.3.6.1.4.1.19376.1.5.3.1.4.5" 471 | ], 472 | "new": [ 473 | "2.16.840.1.113883.10.20.22.4.4" 474 | ] 475 | }, 476 | 477 | { 478 | "old": [ 479 | "2.16.840.1.113883.10.20.1.51", 480 | "1.3.6.1.4.1.19376.1.5.3.1.4.1.2" 481 | ], 482 | "new": [ 483 | "2.16.840.1.113883.10.20.22.4.5" 484 | ] 485 | }, 486 | 487 | { 488 | "old": [ 489 | "2.16.840.1.113883.10.20.1.50", 490 | "1.3.6.1.4.1.19376.1.5.3.1.4.1.1" 491 | ], 492 | "new": [ 493 | "2.16.840.1.113883.10.20.22.4.6" 494 | ] 495 | }, 496 | 497 | { 498 | "old": [ 499 | "2.16.840.1.113883.10.20.22.4.7", 500 | "2.16.840.1.113883.10.20.1.18", 501 | "1.3.6.1.4.1.19376.1.5.3.1.4.5.3", 502 | "2.16.840.1.113883.3.88.11.83.6" 503 | ], 504 | "new": [ 505 | "2.16.840.1.113883.10.20.22.4.7" 506 | ] 507 | }, 508 | 509 | { 510 | "old": [ 511 | "2.16.840.1.113883.10.20.1.55", 512 | "1.3.6.1.4.1.19376.1.5.3.1.4.1" 513 | ], 514 | "new": [ 515 | "2.16.840.1.113883.10.20.22.4.8" 516 | ] 517 | }, 518 | 519 | { 520 | "old": [ 521 | "2.16.840.1.113883.10.20.1.54" 522 | ], 523 | "new": [ 524 | "2.16.840.1.113883.10.20.22.4.9" 525 | ] 526 | }, 527 | 528 | { 529 | "old": [ 530 | "2.16.840.1.113883.10.20.1.29" 531 | ], 532 | "new": [ 533 | "2.16.840.1.113883.10.20.22.4.12", 534 | "2.16.840.1.113883.10.20.22.4.13" 535 | ] 536 | }, 537 | 538 | { 539 | "old": [ 540 | "2.16.840.1.113883.10.20.1.29", 541 | "1.3.6.1.4.1.19376.1.5.3.1.4.19", 542 | "2.16.840.1.113883.3.88.11.83.17" 543 | ], 544 | "new": [ 545 | "2.16.840.1.113883.10.20.22.4.14" 546 | ] 547 | }, 548 | 549 | { 550 | "old": [ 551 | "2.16.840.1.113883.10.20.1.24", 552 | "1.3.6.1.4.1.19376.1.5.3.1.4.12", 553 | "2.16.840.1.113883.3.88.11.83.13" 554 | ], 555 | "new": [ 556 | "2.16.840.1.113883.10.20.22.4.52" 557 | ] 558 | }, 559 | 560 | { 561 | "old": [ 562 | "2.16.840.1.113883.10.20.1.24", 563 | "1.3.6.1.4.1.19376.1.5.3.1.4.7", 564 | "2.16.840.1.113883.3.88.11.83.8" 565 | ], 566 | "new": [ 567 | "2.16.840.1.113883.10.20.22.4.16" 568 | ] 569 | }, 570 | 571 | { 572 | "old": [ 573 | "2.16.840.1.113883.10.20.1.3", 574 | "1.3.6.1.4.1.19376.1.5.3.1.4.7.3", 575 | "2.16.840.1.113883.3.88.11.83.8.3" 576 | ], 577 | "new": [ 578 | "2.16.840.1.113883.10.20.22.4.17" 579 | ] 580 | }, 581 | 582 | { 583 | "old": [ 584 | "2.16.840.1.113883.10.20.1.34", 585 | "1.3.6.1.4.1.19376.1.5.3.1.4.7.3" 586 | ], 587 | "new": [ 588 | "2.16.840.1.113883.10.20.22.4.18" 589 | ] 590 | }, 591 | 592 | { 593 | "old": [ 594 | "2.16.840.1.113883.3.88.11.83.138" 595 | ], 596 | "new": [ 597 | "2.16.840.1.113883.10.20.22.4.19" 598 | ] 599 | }, 600 | 601 | { 602 | "old": [ 603 | "2.16.840.1.113883.10.20.1.49", 604 | "1.3.6.1.4.1.19376.1.5.3.1.4.3" 605 | ], 606 | "new": [ 607 | "2.16.840.1.113883.10.20.22.4.20" 608 | ] 609 | }, 610 | 611 | { 612 | "old": [ 613 | "2.16.840.1.113883.10.20.1.53", 614 | "1.3.6.1.4.1.19376.1.5.3.1.4.7.2", 615 | "2.16.840.1.113883.3.88.11.83.8.2" 616 | ], 617 | "new": [ 618 | "2.16.840.1.113883.10.20.22.4.23" 619 | ] 620 | }, 621 | 622 | { 623 | "old": [ 624 | "2.16.840.1.113883.10.20.1.35", 625 | "1.3.6.1.4.1.19376.1.5.3.1.4.13.1" 626 | ], 627 | "new": [ 628 | "2.16.840.1.113883.10.20.22.4.26" 629 | ] 630 | }, 631 | 632 | { 633 | "old": [ 634 | "1.3.6.1.4.1.19376.1.5.3.1.4.13.2" 635 | ], 636 | "new": [ 637 | "2.16.840.1.113883.10.20.22.4.27" 638 | ] 639 | }, 640 | 641 | { 642 | "old": [ 643 | "2.16.840.1.113883.10.20.1.39" 644 | ], 645 | "new": [ 646 | "2.16.840.1.113883.10.20.22.4.28" 647 | ] 648 | }, 649 | 650 | { 651 | "old": [ 652 | "TODO: erratum in C-CDA p. 537 -- 2.16.840.1.113883.10.20.22.4.38" 653 | ], 654 | "new": [ 655 | "2.16.840.1.113883.10.20.22.4.31" 656 | ] 657 | }, 658 | 659 | { 660 | "old": [ 661 | "2.16.840.1.113883.10.20.1.45" 662 | ], 663 | "new": [ 664 | "2.16.840.1.113883.10.20.22.4.3" 665 | ] 666 | }, 667 | 668 | { 669 | "old": [ 670 | "2.16.840.1.113883.10.20.1.33", 671 | "2.16.840.1.113883.3.88.11.83.19", 672 | "1.3.6.1.4.1.19376.1.5.3.1.4.13.4" 673 | ], 674 | "new": [ 675 | "2.16.840.1.113883.10.20.22.4.38" 676 | ] 677 | }, 678 | 679 | { 680 | "old": [ 681 | "2.16.840.1.113883.10.20.1.23", 682 | "2.16.840.1.113883.3.88.11.83.18" 683 | ], 684 | "new": [ 685 | "2.16.840.1.113883.10.20.22.4.45" 686 | ] 687 | }, 688 | 689 | { 690 | "old": [ 691 | "2.16.840.1.113883.10.20.1.22" 692 | ], 693 | "new": [ 694 | "2.16.840.1.113883.10.20.22.4.46" 695 | ] 696 | }, 697 | 698 | { 699 | "old": [ 700 | "2.16.840.1.113883.10.20.1.4" 701 | ], 702 | "new": [ 703 | "2.16.840.1.113883.10.20.22.4.47" 704 | ] 705 | }, 706 | 707 | { 708 | "old": [ 709 | "2.16.840.1.113883.10.20.1.17" 710 | ], 711 | "new": [ 712 | "2.16.840.1.113883.10.20.22.4.48" 713 | ] 714 | }, 715 | 716 | { 717 | "old": [ 718 | "2.16.840.1.113883.10.20.1.40", 719 | "2.16.840.1.113883.3.88.11.83.11", 720 | "1.3.6.1.4.1.19376.1.5.3.1.4.2" 721 | ], 722 | "new": [ 723 | "2.16.840.1.113883.10.20.22.4.64" 724 | ] 725 | } 726 | ] 727 | 728 | -------------------------------------------------------------------------------- /rubrics/shared/ucum.extract.peg: -------------------------------------------------------------------------------- 1 | /* 2 | * Classic example grammar, which recognizes simple arithmetic expressions like 3 | * "2*(3+4)". The parser generated from this grammar then computes their value. 4 | */ 5 | { 6 | var metric = { 7 | 'Y': 24, 8 | 'Z': 21, 9 | 'E': 18, 10 | 'P': 15, 11 | 'T': 12, 12 | 'G': 9, 13 | 'M': 6, 14 | 'k': 3, 15 | 'h': 2, 16 | 'da': 1, 17 | 'd': -1, 18 | 'c': -2, 19 | 'm': -3, 20 | 'u': -6, 21 | 'n': -9, 22 | 'p': -12, 23 | 'f': -15, 24 | 'a': -18, 25 | 'z': -21, 26 | 'y': -24 27 | }; 28 | } 29 | 30 | start 31 | = term:term { 32 | var constant = 1; 33 | var likes = { }; 34 | term.forEach(function(t){ 35 | if (t.unit === 1) { 36 | return constant *= Math.pow(t.constant, t.exponent); 37 | } 38 | 39 | if (!likes[t.unit]){ 40 | likes[t.unit] = { 41 | unit: t.unit,constant:1,exponent:0 42 | } 43 | } 44 | 45 | likes[t.unit].constant *= t.constant; 46 | likes[t.unit].exponent += t.exponent; 47 | }); 48 | 49 | Object.keys(likes).forEach(function(k){ 50 | constant *= likes[k].constant; 51 | delete likes[k].constant; 52 | }); 53 | 54 | likes = Object.keys(likes) 55 | .filter(function(l){return likes[l].exponent !== 0;}) 56 | .map(function(l){return likes[l];}); 57 | return { 58 | constant: constant, 59 | units: likes.length>0?likes:null 60 | } 61 | } 62 | 63 | sign = sign:[+-] 64 | digit = [0-9] 65 | digits = d:digit dd:digits {return parseInt(d+dd);}/ d:digit {return parseInt(d);} 66 | factor = digits 67 | exponent = s:sign d:digits {return s=='+'?d:-d;}/ d:digits {return d} 68 | simpleUnit = p:PREFIX m:METRICATOM { 69 | console.log("simple",p,m); 70 | m.constant *= Math.pow(10,metric[p]); 71 | return m; 72 | } / ATOM/ METRICATOM / d:digits {return {type:"v",constant:d,exponent:1,unit:1}} 73 | annotatable = u:simpleUnit e:exponent {u.exponent = e; return [u];} / u:simpleUnit{return [u];} 74 | component = 75 | a:annotatable annotation {return a;}/ 76 | annotatable / 77 | a:annotation {return [a];} / 78 | factor / 79 | "(" t:term ")" { return t; } 80 | 81 | term = 82 | "/" t:term {t.forEach(function(c){c.exponent *= -1;}); return t;}/ 83 | c1:component c2:([\./] component)* { 84 | var ret = c1; 85 | c2.forEach(function(c){ 86 | var exp = c[0] === '.' ? 1 : -1; 87 | ret.push.apply(ret,c[1].map(function(c){c.exponent *= exp; return c;})); 88 | }); 89 | console.log(".Args", arguments,c1,c2); 90 | return ret; 91 | } 92 | /c1:component c2:("." component)* { 93 | var ret = c1; 94 | c2.forEach(function(c){ 95 | ret.push.apply(ret,c[1]); 96 | }); 97 | console.log(".Args", arguments,c1,c2); 98 | return ret; 99 | }/ 100 | annotation = "{" m:[^}]+ "}" 101 | {return {type:"v",unit:1,exponent:1,constant:1}} 102 | 103 | ATOM = "10*"/"10^"/"[pi]"/"%"/"[ppth]"/"[pppm]" 104 | /"bit_s" 105 | /"Ao" 106 | /"b" 107 | /"att" 108 | /"[psi]" 109 | /"circ" 110 | /"sph" 111 | /"[car_m]" 112 | /"[car_Au]" 113 | /"[smoot]" 114 | 115 | 116 | /"[pH]" 117 | /"[S]" 118 | /"[HPF]" 119 | /"[LPF]" 120 | /"[arb'U]" 121 | /"[USP'U]" 122 | /"[GPL'U]" 123 | /"[MPL'U]" 124 | /"[APL'U]" 125 | /"[beth'U]" 126 | /"[todd'U]" 127 | /"[dye'U]" 128 | /"[smgy'U]" 129 | /"[bdsk'U]" 130 | /"[ka'U]" 131 | /"[knk'U]" 132 | /"[mclg'U]" 133 | /"[tb'U]" 134 | /"[CCID_50]" 135 | /"[TCID_50]" 136 | /"[PFU]" 137 | /"[FFU]" 138 | /"[CFU]" 139 | /"[BAU]" 140 | /"[AU]" 141 | /"[Amb'a'1'U]" 142 | /"[PNU]" 143 | /"[Lf]" 144 | /"[D'ag'U]" 145 | 146 | /"[in_i'H2O]" 147 | /"[in_i'Hg]" 148 | /"[PRU]" 149 | /"[wood'U]" 150 | /"[diop]" 151 | /"[p'diop]" 152 | /"%[slope]" 153 | /"[mesh_i]" 154 | /"[Ch]" 155 | /"[drp]" 156 | /"[hnsf'U]" 157 | /"[MET]" 158 | /"[hp'_X]" 159 | /"[hp'_C]" 160 | /"[hp'_M]" 161 | /"[hp'_Q]" 162 | /"[hp_X]" 163 | /"[hp_C]" 164 | /"[hp_M]" 165 | /"[hp_Q]" 166 | /"[kp_X]" 167 | /"[kp_C]" 168 | /"[kp_M]" 169 | /"[kp_Q]" 170 | 171 | 172 | /"[degF]" 173 | /"[Cal]" 174 | /"[Btu_39]" 175 | /"[Btu_59]" 176 | /"[Btu_60]" 177 | /"[Btu_m]" 178 | /"[Btu_IT]" 179 | /"[Btu_th]" 180 | /"[Btu]" 181 | /"[HP]" 182 | 183 | 184 | /"[sc_ap]" 185 | /"[dr_ap]" 186 | /"[oz_ap]" 187 | /"[lb_ap]" 188 | 189 | /"[sc_ap]" 190 | /"[dr_ap]" 191 | /"[oz_ap]" 192 | /"[lb_ap]" 193 | 194 | /"[pwt_tr]" 195 | /"[oz_tr]" 196 | /"[lb_tr]" 197 | 198 | /"[gr]" 199 | /"[lb_av]" 200 | /"[oz_av]" 201 | /"[dr_av]" 202 | /"[scwt_av]" 203 | /"[lcwt_av]" 204 | /"[ston_av]" 205 | /"[lton_av]" 206 | /"[stone_av]" 207 | 208 | /"[gal_br]" 209 | /"[pk_br]" 210 | /"[bu_br]" 211 | /"[qt_br]" 212 | /"[pt_br]" 213 | /"[gil_br]" 214 | /"[foz_br]" 215 | /"[fdr_br]" 216 | /"[min_br]" 217 | 218 | /"[gal_us]" 219 | /"[bbl_us]" 220 | /"[qt_us]" 221 | /"[pt_us]" 222 | /"[gil_us]" 223 | /"[foz_us]" 224 | /"[fdr_us]" 225 | /"[min_us]" 226 | /"[crd_us]" 227 | /"[bu_us]" 228 | /"[gal_wi]" 229 | /"[pk_us]" 230 | /"[dqt_us]" 231 | /"[dpt_us]" 232 | /"[tbs_us]" 233 | /"[tsp_us]" 234 | /"[cup_us]" 235 | 236 | /"[in_br]" 237 | /"[ft_br]" 238 | /"[rd_br]" 239 | /"[ch_br]" 240 | /"[lk_br]" 241 | /"[fth_br]" 242 | /"[pc_br]" 243 | /"[yd_br]" 244 | /"[mi_br]" 245 | /"[nmi_br]" 246 | /"[kn_br]" 247 | /"[acr_br]" 248 | 249 | /"[ft_us]" 250 | /"[yd_us]" 251 | /"[in_us]" 252 | /"[rd_us]" 253 | /"[ch_us]" 254 | /"[lk_us]" 255 | /"[rch_us]" 256 | /"[rlk_us]" 257 | /"[fth_us]" 258 | /"[fur_us]" 259 | /"[mi_us]" 260 | /"[acr_us]" 261 | /"[srd_us]" 262 | /"[smi_us]" 263 | /"[sct]" 264 | /"[twp]" 265 | /"[mil_us]" 266 | 267 | /"[in_i]" 268 | /"[ft_i]" 269 | /"[yd_i]" 270 | /"[mi_i]" 271 | /"[fth_i]" 272 | /"[nmi_i]" 273 | /"[kn_i]" 274 | /"[sin_i]" 275 | /"[sft_i]" 276 | /"[syd_i]" 277 | /"[cin_i]" 278 | /"[cft_i]" 279 | /"[cyd_i]" 280 | /"[bf_i]" 281 | /"[cr_i]" 282 | /"[mil_i]" 283 | /"[cml_i]" 284 | /"[hd_i]" 285 | /"[ppb]" 286 | /"[pptr]" 287 | /"gon" 288 | /"deg" 289 | /"'" 290 | /"''" 291 | /"min" 292 | /"h" 293 | /"d" 294 | /"a_t" 295 | /"a_j" 296 | /"a_g" 297 | /"a" 298 | /"wk" 299 | /"mo_s" 300 | /"mo_j" 301 | /"mo_g" 302 | /"mo" 303 | /"AU" 304 | /"atm" 305 | /"[lbf_av]" 306 | 307 | 308 | 309 | 310 | METRICATOM = 311 | "bit" 312 | /"By" 313 | /"Bd" 314 | /"st" 315 | /"mho" 316 | 317 | /"Np" 318 | /"B" 319 | /"B[SPL]" 320 | /"B[V]" 321 | /"B[mV]" 322 | /"B[uV]" 323 | /"B[10.nV]" 324 | /"B[W]" 325 | /"B[kW]" 326 | 327 | /"eq" 328 | /"osm" 329 | /"g%" 330 | /"kat" 331 | /"U" 332 | /"[iU]" 333 | /"[IU]" 334 | 335 | /"m[H2O]" 336 | /"m[Hg]" 337 | /"cal_[15]" 338 | /"cal_[20]" 339 | /"cal_m" 340 | /"cal_IT" 341 | /"cal_th" 342 | /"cal" 343 | 344 | /"Ky" 345 | /"Gal" 346 | /"dyn" 347 | /"erg" 348 | /"P" 349 | /"Bi" 350 | /"St" 351 | /"Mx" 352 | /"G" 353 | /"Oe" 354 | /"Gb" 355 | /"sb" 356 | /"Lmb" 357 | /"ph" 358 | /"Ci" 359 | /"R" 360 | /"RAD" 361 | /"REM" 362 | /"mol" 363 | /"sr" 364 | /"Hz" 365 | /"N" 366 | /"Pa" 367 | /"J" 368 | /"W" 369 | /"A" 370 | /"V" 371 | /"F" 372 | /"Ohm" 373 | /"S" 374 | /"Wb" 375 | /"Cel" 376 | /"T" 377 | /"H" 378 | /"lm" 379 | /"lx" 380 | /"Bq" 381 | /"Gy" 382 | /"Sv" 383 | /"l" 384 | /"L" 385 | /"ar" 386 | /"t" 387 | /"bar" 388 | /"u" 389 | /"eV" 390 | /"pc" 391 | /"[c]" 392 | /"[h]" 393 | /"[k]" 394 | /"[eps_0]" 395 | /"[mu_0]" 396 | /"[e]" 397 | /"[m_e]" 398 | /"[m_p]" 399 | /"[G]" 400 | /"[g]" 401 | /"[ly]" 402 | /"gf" 403 | /"g"{ 404 | return { 405 | type:"v",exponent:1,constant:1,unit:"g" 406 | } 407 | } 408 | /"m" 409 | /"s"/"rad"/"K"/"C"/"cd" 410 | 411 | PREFIX = 412 | "Y"/"Z"/"E"/"P"/"T"/"G"/"M"/"k"/"h"/"da"/"d"/"c"/"m"/"u"/"n"/"p"/"f"/"a"/"z"/"y"/"Ki"/"Mi"/"Gi"/"Ti" 413 | 414 | -------------------------------------------------------------------------------- /rubrics/shared/ucum.peg: -------------------------------------------------------------------------------- 1 | /* 2 | * Classic example grammar, which recognizes simple arithmetic expressions like 3 | * "2*(3+4)". The parser generated from this grammar then computes their value. 4 | */ 5 | 6 | start 7 | = term 8 | 9 | sign = sign:[+-] 10 | digit = [0-9] 11 | digits = d:digit dd:digits {return d+dd;}/ digit 12 | factor = digits 13 | exponent = s:sign d:digits {return "e"+s+d}/ d:digits {return "e"+d} 14 | simpleUnit = p:PREFIX m:METRICATOM { 15 | var val = { 16 | 'Y': 24, 17 | 'Z': 21, 18 | 'E': 18, 19 | 'P': 15, 20 | 'T': 12, 21 | 'G': 9, 22 | 'M': 6, 23 | 'k': 3, 24 | 'h': 2, 25 | 'da': 1, 26 | 'd': -1, 27 | 'c': -2, 28 | 'm': -3, 29 | 'u': -6, 30 | 'n': -9, 31 | 'p': -12, 32 | 'f': -15, 33 | 'a': -18, 34 | 'z': -21, 35 | 'y': -24 36 | }; 37 | return m+val[p] 38 | } / ATOM/ METRICATOM / digits 39 | annotatable = simpleUnit exponent / simpleUnit 40 | component = 41 | annotatable annotation / 42 | annotatable / 43 | annotation / 44 | factor / 45 | "(" term ")" 46 | 47 | term = 48 | "/" component / 49 | component "." term / 50 | component "/" term / 51 | component 52 | 53 | annotation = "{" m:[^}]+ "}" 54 | {return "{"+m.join("")+"}"} 55 | 56 | ATOM = "10*"/"10^"/"[pi]"/"%"/"[ppth]"/"[pppm]" 57 | /"bit_s" 58 | /"Ao" 59 | /"b" 60 | /"att" 61 | /"[psi]" 62 | /"circ" 63 | /"sph" 64 | /"[car_m]" 65 | /"[car_Au]" 66 | /"[smoot]" 67 | 68 | 69 | /"[pH]" 70 | /"[S]" 71 | /"[HPF]" 72 | /"[LPF]" 73 | /"[arb'U]" 74 | /"[USP'U]" 75 | /"[GPL'U]" 76 | /"[MPL'U]" 77 | /"[APL'U]" 78 | /"[beth'U]" 79 | /"[todd'U]" 80 | /"[dye'U]" 81 | /"[smgy'U]" 82 | /"[bdsk'U]" 83 | /"[ka'U]" 84 | /"[knk'U]" 85 | /"[mclg'U]" 86 | /"[tb'U]" 87 | /"[CCID_50]" 88 | /"[TCID_50]" 89 | /"[PFU]" 90 | /"[FFU]" 91 | /"[CFU]" 92 | /"[BAU]" 93 | /"[AU]" 94 | /"[Amb'a'1'U]" 95 | /"[PNU]" 96 | /"[Lf]" 97 | /"[D'ag'U]" 98 | 99 | /"[in_i'H2O]" 100 | /"[in_i'Hg]" 101 | /"[PRU]" 102 | /"[wood'U]" 103 | /"[diop]" 104 | /"[p'diop]" 105 | /"%[slope]" 106 | /"[mesh_i]" 107 | /"[Ch]" 108 | /"[drp]" 109 | /"[hnsf'U]" 110 | /"[MET]" 111 | /"[hp'_X]" 112 | /"[hp'_C]" 113 | /"[hp'_M]" 114 | /"[hp'_Q]" 115 | /"[hp_X]" 116 | /"[hp_C]" 117 | /"[hp_M]" 118 | /"[hp_Q]" 119 | /"[kp_X]" 120 | /"[kp_C]" 121 | /"[kp_M]" 122 | /"[kp_Q]" 123 | 124 | 125 | /"[degF]" 126 | /"[Cal]" 127 | /"[Btu_39]" 128 | /"[Btu_59]" 129 | /"[Btu_60]" 130 | /"[Btu_m]" 131 | /"[Btu_IT]" 132 | /"[Btu_th]" 133 | /"[Btu]" 134 | /"[HP]" 135 | 136 | 137 | /"[sc_ap]" 138 | /"[dr_ap]" 139 | /"[oz_ap]" 140 | /"[lb_ap]" 141 | 142 | /"[sc_ap]" 143 | /"[dr_ap]" 144 | /"[oz_ap]" 145 | /"[lb_ap]" 146 | 147 | /"[pwt_tr]" 148 | /"[oz_tr]" 149 | /"[lb_tr]" 150 | 151 | /"[gr]" 152 | /"[lb_av]" 153 | /"[oz_av]" 154 | /"[dr_av]" 155 | /"[scwt_av]" 156 | /"[lcwt_av]" 157 | /"[ston_av]" 158 | /"[lton_av]" 159 | /"[stone_av]" 160 | 161 | /"[gal_br]" 162 | /"[pk_br]" 163 | /"[bu_br]" 164 | /"[qt_br]" 165 | /"[pt_br]" 166 | /"[gil_br]" 167 | /"[foz_br]" 168 | /"[fdr_br]" 169 | /"[min_br]" 170 | 171 | /"[gal_us]" 172 | /"[bbl_us]" 173 | /"[qt_us]" 174 | /"[pt_us]" 175 | /"[gil_us]" 176 | /"[foz_us]" 177 | /"[fdr_us]" 178 | /"[min_us]" 179 | /"[crd_us]" 180 | /"[bu_us]" 181 | /"[gal_wi]" 182 | /"[pk_us]" 183 | /"[dqt_us]" 184 | /"[dpt_us]" 185 | /"[tbs_us]" 186 | /"[tsp_us]" 187 | /"[cup_us]" 188 | 189 | /"[in_br]" 190 | /"[ft_br]" 191 | /"[rd_br]" 192 | /"[ch_br]" 193 | /"[lk_br]" 194 | /"[fth_br]" 195 | /"[pc_br]" 196 | /"[yd_br]" 197 | /"[mi_br]" 198 | /"[nmi_br]" 199 | /"[kn_br]" 200 | /"[acr_br]" 201 | 202 | /"[ft_us]" 203 | /"[yd_us]" 204 | /"[in_us]" 205 | /"[rd_us]" 206 | /"[ch_us]" 207 | /"[lk_us]" 208 | /"[rch_us]" 209 | /"[rlk_us]" 210 | /"[fth_us]" 211 | /"[fur_us]" 212 | /"[mi_us]" 213 | /"[acr_us]" 214 | /"[srd_us]" 215 | /"[smi_us]" 216 | /"[sct]" 217 | /"[twp]" 218 | /"[mil_us]" 219 | 220 | /"[in_i]" 221 | /"[ft_i]" 222 | /"[yd_i]" 223 | /"[mi_i]" 224 | /"[fth_i]" 225 | /"[nmi_i]" 226 | /"[kn_i]" 227 | /"[sin_i]" 228 | /"[sft_i]" 229 | /"[syd_i]" 230 | /"[cin_i]" 231 | /"[cft_i]" 232 | /"[cyd_i]" 233 | /"[bf_i]" 234 | /"[cr_i]" 235 | /"[mil_i]" 236 | /"[cml_i]" 237 | /"[hd_i]" 238 | /"[ppb]" 239 | /"[pptr]" 240 | /"gon" 241 | /"deg" 242 | /"'" 243 | /"''" 244 | /"min" 245 | /"h" 246 | /"d" 247 | /"a_t" 248 | /"a_j" 249 | /"a_g" 250 | /"a" 251 | /"wk" 252 | /"mo_s" 253 | /"mo_j" 254 | /"mo_g" 255 | /"mo" 256 | /"AU" 257 | /"atm" 258 | /"[lbf_av]" 259 | 260 | 261 | 262 | 263 | METRICATOM = 264 | "bit" 265 | /"By" 266 | /"Bd" 267 | /"st" 268 | /"mho" 269 | 270 | /"Np" 271 | /"B" 272 | /"B[SPL]" 273 | /"B[V]" 274 | /"B[mV]" 275 | /"B[uV]" 276 | /"B[10.nV]" 277 | /"B[W]" 278 | /"B[kW]" 279 | 280 | /"eq" 281 | /"osm" 282 | /"g%" 283 | /"kat" 284 | /"U" 285 | /"[iU]" 286 | /"[IU]" 287 | 288 | /"m[H2O]" 289 | /"m[Hg]" 290 | /"cal_[15]" 291 | /"cal_[20]" 292 | /"cal_m" 293 | /"cal_IT" 294 | /"cal_th" 295 | /"cal" 296 | 297 | /"Ky" 298 | /"Gal" 299 | /"dyn" 300 | /"erg" 301 | /"P" 302 | /"Bi" 303 | /"St" 304 | /"Mx" 305 | /"G" 306 | /"Oe" 307 | /"Gb" 308 | /"sb" 309 | /"Lmb" 310 | /"ph" 311 | /"Ci" 312 | /"R" 313 | /"RAD" 314 | /"REM" 315 | /"mol" 316 | /"sr" 317 | /"Hz" 318 | /"N" 319 | /"Pa" 320 | /"J" 321 | /"W" 322 | /"A" 323 | /"V" 324 | /"F" 325 | /"Ohm" 326 | /"S" 327 | /"Wb" 328 | /"Cel" 329 | /"T" 330 | /"H" 331 | /"lm" 332 | /"lx" 333 | /"Bq" 334 | /"Gy" 335 | /"Sv" 336 | /"l" 337 | /"L" 338 | /"ar" 339 | /"t" 340 | /"bar" 341 | /"u" 342 | /"eV" 343 | /"pc" 344 | /"[c]" 345 | /"[h]" 346 | /"[k]" 347 | /"[eps_0]" 348 | /"[mu_0]" 349 | /"[e]" 350 | /"[m_e]" 351 | /"[m_p]" 352 | /"[G]" 353 | /"[g]" 354 | /"[ly]" 355 | /"gf" 356 | /"g" 357 | /"m" 358 | /"s"/"rad"/"K"/"C"/"cd" 359 | 360 | PREFIX = 361 | "Y"/"Z"/"E"/"P"/"T"/"G"/"M"/"k"/"h"/"da"/"d"/"c"/"m"/"u"/"n"/"p"/"f"/"a"/"z"/"y"/"Ki"/"Mi"/"Gi"/"Ti" 362 | 363 | -------------------------------------------------------------------------------- /rubrics/smoking-03-structured-smoking-status.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | var xpath = common.xpath; 3 | 4 | var rubric = module.exports = function(){}; 5 | 6 | rubric.prototype.report = function(done){ 7 | var ccda = this.manager.ccda; 8 | var report; 9 | 10 | var good = xpath(ccda, correctSmokingObservation).length; 11 | var bad = xpath(ccda, wrongSmokingObservation).length; 12 | 13 | report = common.report(rubric, good, good+bad); 14 | 15 | done(null, report); 16 | }; 17 | 18 | var wrongSmokingObservation = "//h:templateId[@root='2.16.840.1.113883.10.22.4.78']/.."; 19 | var correctSmokingObservation = "//h:templateId[@root='2.16.840.1.113883.10.20.22.4.78']/.."; 20 | -------------------------------------------------------------------------------- /rubrics/smoking-03-structured-smoking-status.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["Social History", "Smoking"], 4 | "description": "Smoking Status Observations have the correct template ID", 5 | "detail": "Smoking status observations should have template ID 2.16.840.1.113883.10.20.22.4.78", 6 | "maxPoints": 1, 7 | "points": { 8 | "1": "Smoking status is recorded in a discrete observation", 9 | "0": "Smoking status is recorded in a non-discrete observation" 10 | }, 11 | "doesNotApply": "No smoking status in document" 12 | } 13 | -------------------------------------------------------------------------------- /rubrics/smokingcodes.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | var xpath = common.xpath; 3 | 4 | var validSmokingCodes = [ 5 | "449868002", // Current every day smoker 6 | "428041000124106", // Current some day smoker 7 | "8517006", // Former smoker 8 | "266919005", // Never smoker 9 | "77176002", // Smoker, current status unknown 10 | "266927001", // Unknown if ever smoked 11 | "428071000124103", //Heavy tobacco smoker 12 | "428061000124105" // Light tobacco smoker 13 | ]; 14 | 15 | var rubric = module.exports = function(){}; 16 | 17 | rubric.prototype.report = function(done){ 18 | var ccda = this.manager.ccda; 19 | var vocab = this.manager.vocab; 20 | 21 | var codes = common.extractCodes(ccda, codeXpath, vocab); 22 | 23 | var hits = []; 24 | var misses = []; 25 | 26 | codes.forEach(function(v){ 27 | if (!v.normalized || v.normalized.codeSystemName !== "SNOMED-CT"){ 28 | return misses.push(v); 29 | } 30 | 31 | if (validSmokingCodes.indexOf(v.code) === -1){ 32 | return misses.push(v); 33 | } 34 | 35 | return hits.push(v); 36 | }); 37 | 38 | var report = common.report(rubric, hits.length, codes.length, { 39 | hits: hits, 40 | misses: misses 41 | }); 42 | 43 | done(null, report); 44 | }; 45 | 46 | var codeXpath = "//h:templateId[@root='2.16.840.1.113883.10.20.22.4.78']/../h:value"; 47 | -------------------------------------------------------------------------------- /rubrics/smokingcodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["ccda"], 3 | "category": ["Social History", "Smoking"], 4 | "description": "Structured Smoking Status Observations use correct SNOMED CT Codes", 5 | "detail": "Smoking Status obervations should be coded according to an explicit list of eight SNOMED CT Codes.", 6 | "maxPoints": 3, 7 | "points": { 8 | "5": "Smoking statuses are recorded with correct SNOMED CT codes", 9 | "0": "Smoking status are not recorded with correct SNOMED CT codes" 10 | }, 11 | "doesNotApply": "No smoking status in document" 12 | } 13 | -------------------------------------------------------------------------------- /rubrics/smokingcodes.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | smoking status codes weren't drawn from the eight recommended codes. 6 |

7 | 8 |

9 | Recommended codes: 10 |

17 |

18 | 19 | <% include partials/codeTable%> 20 | 21 | <%}%> 22 | -------------------------------------------------------------------------------- /rubrics/smokingstructure.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | var xpath = common.xpath; 3 | 4 | var rubric = module.exports = function(){}; 5 | 6 | rubric.prototype.report = function(done){ 7 | var ccda = this.manager.ccda; 8 | var report; 9 | 10 | var social = xpath(ccda, socialHistory).toString(); 11 | if (social.length == 0){ 12 | report = common.report(rubric, 0, 0); 13 | } else if (social.match(/smoking/i) || social.match(/smoker/i)){ 14 | report = common.report(rubric, 0, 1); 15 | } else { 16 | report = common.report(rubric, 1, 1); 17 | } 18 | 19 | done(null, report); 20 | }; 21 | 22 | var templateIds = { 23 | socialHistoryObservation: "2.16.840.1.113883.10.20.22.4.38" 24 | }; 25 | 26 | var socialHistory = "//h:templateId[@root='"+templateIds.socialHistoryObservation+"']/.."; 27 | -------------------------------------------------------------------------------- /rubrics/smokingstructure.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["Social History", "Smoking"], 4 | "description": "Only structured Smoking Status Observations are used", 5 | "detail": "Smoking status should be recorded in a discrete 'smoking status observation', not in the more generic, less-computable 'social history observation'.", 6 | "maxPoints": 3, 7 | "points": { 8 | "5": "Smoking status is recorded in a discrete observation", 9 | "0": "Smoking status is recorded in a non-discrete observation" 10 | }, 11 | "doesNotApply": "No smoking status in document" 12 | } 13 | -------------------------------------------------------------------------------- /rubrics/templateids.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | var rubric = module.exports = function(){}; 3 | var templates = require('./shared/templates.json'); 4 | 5 | rubric.prototype.report = function(done){ 6 | 7 | var ccda = this.manager.ccda; 8 | var vocab = this.manager.vocab; 9 | var mistakes = {}; 10 | 11 | templates.forEach(function(t){ 12 | 13 | var oldIds = t.old.map(function(id){ 14 | return "//h:templateId[ @root='"+id+"' ]"; 15 | }).join(" | "), 16 | oldpath = oldIds; 17 | olds = common.xpath(ccda, oldpath), 18 | newPath = t.new.map(function(id){ 19 | return " ../h:templateId[@root='"+id+"']"; 20 | }).join(" | "); 21 | 22 | olds && olds.forEach(function(oldNode){ 23 | var rescued = common.xpath(oldNode, newPath); 24 | if (rescued.length === 0){ 25 | var badId = common.xpath(oldNode, "string(./@root)"); 26 | 27 | console.log("did news"); 28 | if (!mistakes[badId]){ 29 | mistakes[badId] = []; 30 | } 31 | mistakes[badId].push({badId: badId, goodIds: t.new}); 32 | } 33 | }); 34 | }); 35 | 36 | var denominator = templates.length; 37 | var numerator = Object.keys(mistakes).length ? 0 : denominator; 38 | var report = common.report(rubric, numerator, denominator, { 39 | misses: mistakes 40 | }); 41 | 42 | done(null, report); 43 | }; 44 | -------------------------------------------------------------------------------- /rubrics/templateids.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["c-cda", "smart"], 3 | "category": ["General", "Codes"], 4 | "description": "Document uses official C-CDA templateIds whenever possible", 5 | "detail": "C-CDA's prescribed templateIds should be used whenever possible. Additional templateId elements are allowed, but official C-CDA templateIds should always be present when they apply.", 6 | "maxPoints": 5, 7 | "points": { 8 | "5": "Everywhere that old templateIds are used, official C-CDA templateIds are also present", 9 | "0": "C-CDA templateIds are missing" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /rubrics/templateids.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (Object.keys(misses).length > 0) {%> 2 | 3 |

4 | <%= Object.keys(misses).length %> 5 | non-CCDA templateIds were used without C-CDA equivalents 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% Object.keys(misses).forEach(function(miss){ %> 16 | 17 | 20 | 21 | 30 | 31 | <% }) %> 32 |
Old templateIdTimes usedOfficial C-CDA templateId
18 | <%= miss %> 19 | <%= misses[miss].length%> 22 |
    23 | <% misses[miss][0].goodIds.forEach(function(id){ %> 24 |
  • 25 | <%= id %> 26 |
  • 27 | <% }) %> 28 |
29 |
33 | <% } %> 34 | -------------------------------------------------------------------------------- /rubrics/units-01-valid-ucum.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | var ucum = require('./shared/ucum_parser'); 3 | var rubric = module.exports = function(){}; 4 | 5 | rubric.prototype.test = function validUcum(u, context){ 6 | console.log("Args", arguments); 7 | try { ucum.parse(u); } 8 | catch(e){ 9 | var err = "Invalid UCUM unit"; 10 | this.fail(u, context, err); 11 | } 12 | } 13 | 14 | var units = "//*[@value and @unit]"; 15 | 16 | rubric.prototype.inputs = function(){ 17 | return common.xpath(this.manager.ccda, units).map(function(u){ 18 | console.log("units", u); 19 | return [common.xpath(u, "string(./@unit)"), u]; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /rubrics/units-01-valid-ucum.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["c-cda", "smart"], 3 | "category": ["General", "Physical Quantities"], 4 | "description": "Physical units are valid UCUM expressions", 5 | "detail": "Any time a physical unit is used, it should be a valid UCUM expression", 6 | "maxPoints": 3 7 | } 8 | -------------------------------------------------------------------------------- /rubrics/vitals-01-structured.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){}; 4 | 5 | rubric.prototype.report = function(done){ 6 | var ccda = this.manager.ccda; 7 | var report; 8 | vitalsStructured = common.xpath(ccda, "//h:templateId[@root='2.16.840.1.113883.10.20.22.2.4.1']/.."); 9 | 10 | if (vitalsStructured && vitalsStructured.length == 1) { 11 | report = common.report(rubric, 1, 1); 12 | } else { 13 | report = common.report(rubric, 0, 1); 14 | } 15 | 16 | done(null, report); 17 | }; 18 | -------------------------------------------------------------------------------- /rubrics/vitals-01-structured.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["Vitals", "Codes"], 4 | "description": "Vitals are represented using structured entries", 5 | "detail": "Vitals in C-CDA should be represented with individual structured entries corresponding to BP, Heart Rate, etc. ", 6 | "maxPoints": 5, 7 | "points": { 8 | "5": "Vitals are represented with structure", 9 | "0": "Vitals are represnted without structure" 10 | }, 11 | "doesNotApply": "No vitals in document" 12 | } 13 | -------------------------------------------------------------------------------- /rubrics/vitals-02-loinc.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){}; 4 | 5 | var unitMap = require('./shared/codes').unitMap; 6 | 7 | rubric.prototype.report = function(done){ 8 | var ccda = this.manager.ccda; 9 | var vocab = this.manager.vocab; 10 | 11 | var codeXpath = 12 | "(//h:templateId[@root='2.16.840.1.113883.10.20.22.2.4'] | \ 13 | //h:templateId[@root='2.16.840.1.113883.10.20.22.2.4.1'])/.. \ 14 | //h:templateId[@root='2.16.840.1.113883.10.20.22.4.27']/../h:code"; 15 | 16 | var codes = common.extractCodes(ccda, codeXpath, vocab); 17 | 18 | var hits = []; 19 | var misses = []; 20 | 21 | codes.forEach(function(v){ 22 | unitMap[v.code] ? hits.push(v) : misses.push(v); 23 | }); 24 | 25 | var report = common.report(rubric, hits.length, codes.length, { 26 | hits: hits, 27 | misses: misses 28 | }); 29 | 30 | done(null, report); 31 | }; 32 | -------------------------------------------------------------------------------- /rubrics/vitals-02-loinc.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["Vitals", "Codes"], 4 | "description": "Vitals are expressed with LOINC codes", 5 | "detail": "Vitals in C-CDA should be coded with LOINC. Specifically, with codes from the HITSP Vital Sign Result value set.", 6 | "maxPoints": 3, 7 | "points": { 8 | "3": "> 80% of vitals have an LOINC Vital Sign Result code", 9 | "2": "> 50% of vitals have an LOINC Vital Sign Result code", 10 | "1": "At least one medication has an LOINC Vital Sign Result code", 11 | "0": "No vitals have an LOINC Vital Sign Result code" 12 | }, 13 | "doesNotApply": "No vitals in document" 14 | } 15 | -------------------------------------------------------------------------------- /rubrics/vitals-02-loinc.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | vital sign codes weren't in the recommended value set 6 |

7 | 8 |

9 | Note: This may be normal, if this C-CDA document includes unusual 10 | vitals for which no appropriate LOINC code exists. But look through the 11 | un-matched codes below to make sure you don't have a mapping error in your 12 | export pipeline.

13 | 14 |

15 | Recommended value set: 16 |

    17 |
  1. 18 | 19 | HITSP Vital Sign Result 20 | 21 |
  2. 22 |
23 |

24 | 25 | <% include partials/codeTable%> 26 | <% } %> 27 | -------------------------------------------------------------------------------- /rubrics/vitals-03-ucum.js: -------------------------------------------------------------------------------- 1 | var common = require('../lib/common'); 2 | 3 | var rubric = module.exports = function(){}; 4 | 5 | var unitMap = require('./shared/codes').unitMap; 6 | 7 | rubric.prototype.report = function(done){ 8 | var ccda = this.manager.ccda; 9 | var vocab = this.manager.vocab; 10 | var report; 11 | 12 | vitalsStructured = common.xpath( 13 | ccda, 14 | "(//h:templateId[@root='2.16.840.1.113883.10.20.22.2.4.1'] | \ 15 | //h:templateId[@root='2.16.840.1.113883.10.20.22.2.4'])/.. \ 16 | //h:templateId[@root='2.16.840.1.113883.10.20.22.4.27']/.." 17 | ); 18 | 19 | var hits = []; 20 | var misses = []; 21 | 22 | if (vitalsStructured){ 23 | vitalsStructured.forEach(function(v){ 24 | 25 | var code = common.extractCodes(v, "./h:code", vocab)[0]; 26 | 27 | var unit = common.xpath(v, "string(./h:value/@unit)"); 28 | var value = common.xpath(v, "string(./h:value/@value)"); 29 | 30 | if (!unitMap[code.code]) { 31 | return; 32 | } 33 | 34 | var attempt = { 35 | code: code, 36 | value: value, 37 | unit: unit, 38 | preferred: unitMap[code.code].units 39 | }; 40 | 41 | if (unitMap[code.code].units.indexOf(unit) !== -1){ 42 | hits.push(attempt); 43 | } else { 44 | misses.push(attempt); 45 | } 46 | 47 | }); 48 | } 49 | 50 | report = common.report(rubric, hits.length, hits.length+misses.length, { 51 | hits: hits, 52 | misses: misses 53 | } 54 | 55 | ); 56 | 57 | done(null, report); 58 | }; 59 | -------------------------------------------------------------------------------- /rubrics/vitals-03-ucum.json: -------------------------------------------------------------------------------- 1 | { 2 | "scorecards": ["smart"], 3 | "category": ["Vitals", "Codes"], 4 | "description": "Vitals are expressed with UCUM units", 5 | "detail": "Vitals in C-CDA should be represented with physical quantities that have appropriate UCUM codes.", 6 | "maxPoints": 5, 7 | "points": { 8 | "5": "Vitals are represented with appropriate UCUM codes", 9 | "0": "Not all vitals have an appropriate UCUM code" 10 | }, 11 | "doesNotApply": "No vitals in document" 12 | } 13 | -------------------------------------------------------------------------------- /rubrics/vitals-03-ucum.report.ejs: -------------------------------------------------------------------------------- 1 | <% if (misses.length > 0) {%> 2 | 3 |

4 | <%= misses.length %> of <%= (misses.length + hits.length)%> 5 | vital signs weren't recorded with the recommended UCUM units

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% misses.forEach(function(miss){ %> 16 | 17 | 22 | 25 | 26 | 27 | 34 | 37 | 38 | <% }) %> 39 |
VitalValueYour unitsPreferred unitsWhat now?
18 | 19 | <%= miss.code.normalized.codeSystemName %>:<%= miss.code.normalized.code %> 20 | 21 | 23 | <%= miss.code.normalized.conceptName %> 24 | "<%= miss.value %>" <%= miss.unit %> 28 |
    29 | <% miss.preferred.forEach(function(preferred){ %> 30 |
  • <%= preferred %>
  • 31 | <% }); %> 32 |
33 |
35 | Check mapping to UCUM 36 |
40 | <% } %> 41 | -------------------------------------------------------------------------------- /value_sets/README.md: -------------------------------------------------------------------------------- 1 | Attempting to catalog value sets required and recommended in CCDA. 2 | 3 | 1. Value sets available from CDC's `phinvads.cdc.gov` 4 | 2. Value sets explicitly listed in the CCDA spec (but what does it mean when these are "dynamic"?) 5 | 3. Value sets "described" (ucum -- really a language more than a value set; or "look below this point in SNOMED CT") 6 | 7 | To import phinvads vocab: 8 | 9 | ``` 10 | python import_phinvads_vocab.py 11 | ``` 12 | 13 | TODO: resolve discrepency between value set vs. code system for Immunizations. (CCDA is inconsistent). 14 | 15 | Effectively we want all concept from the code set: 2.16.840.1.113883.12.292 16 | ... and we want to call that value set: 2.16.840.1.113883.3.88.12.80.22 (even though phinvads doesn't). 17 | 18 | -------------------------------------------------------------------------------- /value_sets/assign_value_sets.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import pymongo 3 | import MySQLdb, MySQLdb.cursors 4 | from pymongo import Connection 5 | import urllib 6 | 7 | LOINC_TOP2K_VALUE_SET = "LOINC Top 2000" 8 | VAKP_VALUE_SET = "SNOMED VA/KP Problem List" 9 | CORE_PROBLEM_LIST_VALUE_SET = "SNOMED CORE Problem List" 10 | GENERIC_CLINICAL_DRUG = "RxNorm Generic Clinical Drug" 11 | BRANDED_CLINICAL_DRUG = "RxNorm Branded Clinical Drug" 12 | BRAND_NAME = "RxNorm Brand Name" 13 | GENERIC_NAME = "RxNorm Generic Name" 14 | 15 | connection = Connection() 16 | db = connection.vocab 17 | concepts = db.concepts 18 | 19 | # clear all value set flags 20 | 21 | for code in concepts.find({'valueSets': {'$not': {'$size':0}}}): 22 | code['valueSets'] = [] 23 | concepts.update({'_id': code['_id']}, code) 24 | # print code 25 | 26 | print "Fetching codes from LOINC.org" 27 | traw = urllib.urlopen('http://loinc.org/usage/obs/loinc-top-2000-plus-loinc-lab-observations-us.csv') 28 | loincReader = csv.DictReader(traw) 29 | 30 | for code in loincReader: 31 | concepts.update({"_id": "http://purl.bioontology.org/ontology/LNC/"+code['LOINC_NUM']}, {"$addToSet": {"valueSets": LOINC_TOP2K_VALUE_SET}}) 32 | print code 33 | 34 | db = MySQLdb.connect(host="localhost", user="umls", passwd="UMLS", db="umls", cursorclass=MySQLdb.cursors.DictCursor) 35 | 36 | subsetq = """SELECT distinct code FROM umls.MRCONSO WHERE sab='SNOMEDCT_US' and (CVF & %s > 0)""" 37 | 38 | cur = db.cursor() 39 | cur.execute(subsetq%512) 40 | for v in cur.fetchall(): 41 | concepts.update({"_id": "http://purl.bioontology.org/ontology/SNOMEDCT/"+v['code']}, {"$addToSet": {"valueSets": VAKP_VALUE_SET}}) 42 | 43 | cur.execute(subsetq%2048) 44 | for v in cur.fetchall(): 45 | concepts.update({"_id": "http://purl.bioontology.org/ontology/SNOMEDCT/"+v['code']}, {"$addToSet": {"valueSets": CORE_PROBLEM_LIST_VALUE_SET}}) 46 | 47 | cur.execute("""select SCUI, str from umls.MRCONSO where SAB='RXNORM' and TTY in ('SBD', 'BPCK');"""); 48 | for v in cur.fetchall(): 49 | concepts.update({"_id": "http://purl.bioontology.org/ontology/RXNORM/"+v['SCUI']}, {"$addToSet": {"valueSets": BRANDED_CLINICAL_DRUG}}) 50 | 51 | cur.execute("""select SCUI, str from umls.MRCONSO where SAB='RXNORM' and TTY in ('SCD', 'GPCK');"""); 52 | for v in cur.fetchall(): 53 | concepts.update({"_id": "http://purl.bioontology.org/ontology/RXNORM/"+v['SCUI']}, {"$addToSet": {"valueSets": GENERIC_CLINICAL_DRUG}}) 54 | 55 | cur.execute("""select SCUI, str from umls.MRCONSO where SAB='RXNORM' and TTY in ('IN', 'GPCK');"""); 56 | for v in cur.fetchall(): 57 | concepts.update({"_id": "http://purl.bioontology.org/ontology/RXNORM/"+v['SCUI']}, {"$addToSet": {"valueSets": GENERIC_NAME}}) 58 | 59 | cur.execute("""select SCUI, str from umls.MRCONSO where SAB='RXNORM' and TTY in ('BN', 'BPCK');"""); 60 | for v in cur.fetchall(): 61 | concepts.update({"_id": "http://purl.bioontology.org/ontology/RXNORM/"+v['SCUI']}, {"$addToSet": {"valueSets": BRAND_NAME}}) 62 | 63 | 64 | -------------------------------------------------------------------------------- /value_sets/extract_concepts_from_mysql.py: -------------------------------------------------------------------------------- 1 | import MySQLdb, MySQLdb.cursors 2 | import json 3 | import pymongo 4 | import copy 5 | from pymongo import MongoClient 6 | 7 | from nltk.tokenize import word_tokenize 8 | 9 | mconnection = MongoClient() 10 | mdb = mconnection.vocab 11 | concepts = mdb.concepts 12 | 13 | db = MySQLdb.connect(host="localhost", user="umls", passwd="UMLS", db="umls", cursorclass=MySQLdb.cursors.DictCursor) 14 | 15 | 16 | COUNT=0 17 | 18 | systems = [ 19 | { 20 | 'umls_sab': 'SNOMEDCT', 21 | 'oid': '2.16.840.1.113883.6.96', 22 | 'sab': 'SNOMED-CT', 23 | 'tty': ['PT'], 24 | 'url': 'http://purl.bioontology.org/ontology/SNOMEDCT/' 25 | }, 26 | { 27 | 'umls_sab': 'SCTUSX', 28 | 'oid': '2.16.840.1.113883.6.96', 29 | 'sab': 'SNOMED-CT', 30 | 'tty': ['PT'], 31 | 'url': 'http://purl.bioontology.org/ontology/SNOMEDCT/' 32 | }, 33 | { 34 | 'umls_sab': 'SNOMEDCT_US', 35 | 'oid': '2.16.840.1.113883.6.96', 36 | 'sab': 'SNOMED-CT', 37 | 'tty': ['PT'], 38 | 'url': 'http://purl.bioontology.org/ontology/SNOMEDCT/' 39 | }, 40 | { 41 | 'umls_sab': 'RXNORM', 42 | 'oid': '2.16.840.1.113883.6.88', 43 | 'sab': 'RxNorm', 44 | 'ttynot': ['ST', 'TMSY', 'OCD'], 45 | 'url': 'http://purl.bioontology.org/ontology/RXNORM/' 46 | }, 47 | { 48 | 'umls_sab': 'LNC', 49 | 'oid': '2.16.840.1.113883.6.1', 50 | 'sab': 'LOINC', 51 | 'tty': ['LC'], 52 | 'url': 'http://purl.bioontology.org/ontology/LNC/' 53 | } 54 | ] 55 | 56 | 57 | def getTokens(sab, code): 58 | cur = db.cursor() 59 | det = """select str from umls.MRCONSO where sab='%s' and code='%s';""" 60 | cur.execute(det%(sab, code)) 61 | strs = cur.fetchall() 62 | tokens = set(word_tokenize(" ".join([x['str'] for x in strs]).lower())) 63 | return sorted(filter(lambda x: len(x)>1, tokens)) 64 | 65 | def makeRow(s, v): 66 | ret = {} 67 | ret['_id'] = s['url'] + v['code'] 68 | ret['code'] = v['code'] 69 | ret['codeSystem'] = s['oid'] 70 | ret['codeSystemName'] = s['sab'] 71 | ret['conceptName'] = v['str'] 72 | ret['conceptNameTokens'] = getTokens(s['umls_sab'], v['code']) 73 | ret['valueSets'] = [] 74 | ret['active'] = True 75 | if ('active' in s and (s['active'] == False)): 76 | ret['active'] = False 77 | return ret 78 | 79 | def processSet(cur, query, s): 80 | global COUNT 81 | cur.execute(query) 82 | abunch = cur.fetchmany() 83 | while abunch: 84 | for v in abunch: 85 | COUNT += 1 86 | concept = makeRow(s, v) 87 | concepts.update({'_id': concept['_id']}, concept, upsert=True) 88 | if COUNT%2000 == 0: 89 | print COUNT, concept 90 | abunch = cur.fetchmany() 91 | """ 92 | # assign value sets in an external second pass 93 | if v['cvf'] and (v['cvf'] & 2048): 94 | ret['valueSets'].append("SNOMED CT CORE Problem List") 95 | 96 | if v['cvf'] and (v['cvf'] & 512): 97 | ret['valueSets'].append("VA/KP Problem List") 98 | """ 99 | 100 | cur = db.cursor() 101 | 102 | for s in systems: 103 | allcodes = """select cui, code, sab, str, cvf from umls.MRCONSO where SAB='%s'"""%s['umls_sab'] 104 | if ('ttynot' in s): 105 | allcodes += """ and tty not in (%s) """ % ("'" + "','".join(s['ttynot']) + "'") 106 | if ('tty' in s): 107 | allcodes += """ and tty in (%s) """ % ("'" + "','".join(s['tty']) + "'") 108 | print allcodes 109 | 110 | processSet(cur, allcodes, s) 111 | 112 | print "Finding retired" 113 | snomedRetired = """select c.cui, c.code, c.sab, c.str, c.cvf from umls.MRCONSO c join umls.MRSAT a where 114 | c.sab='SNOMEDCT_US' and a.sab='SNOMEDCT_US' and c.tty='OAP' and a.code=c.SCUI and a.atn='ACTIVE' and a.atv = '0'; 115 | """ 116 | s = copy.deepcopy(systems[0]) 117 | s['active'] = False 118 | processSet(cur, snomedRetired, s) 119 | 120 | 121 | print "Counted", COUNT 122 | -------------------------------------------------------------------------------- /value_sets/import_phinda_vocab.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chb/ccdaScorecard/a6e6641dd529d92147fb330a381c56f6d39654c2/value_sets/import_phinda_vocab.pyc -------------------------------------------------------------------------------- /value_sets/import_phinvads_vocab.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | from mustaine.client import HessianProxy 3 | import mustaine 4 | from pymongo import Connection 5 | 6 | PAGESIZE = 500 7 | 8 | connection = Connection() 9 | db = connection.vocab 10 | value_set_concepts = db.valueSetConcepts 11 | 12 | service = HessianProxy("http://phinvads.cdc.gov/vocabService/v2") 13 | 14 | def toBSON(o): 15 | ret = {} 16 | for k in dir(o): 17 | if k.startswith("_"): continue 18 | v = getattr(o,k) 19 | if type(v) == list: 20 | ret[k] = map(toBSON, v) 21 | elif type(v) == mustaine.protocol.Object: 22 | ret[k] = toBSON(v) 23 | else: 24 | ret[k] = v 25 | return ret 26 | 27 | codeSystems = {} 28 | for cs in db.codeSystems.find({}): 29 | codeSystems[cs['oid']] = cs 30 | 31 | def getCodeSystem(s): 32 | if s in codeSystems: 33 | return codeSystems[s] 34 | 35 | c = service.getCodeSystemByOid(s) 36 | assert c.totalResults == 1, "Got != 1 code system result for oid %s"%s 37 | codeSystems[s] = toBSON(c.codeSystems[0]) 38 | db.codeSystems.insert(codeSystems[s]) 39 | return codeSystems[s] 40 | 41 | def fetchValueSet(vsoid): 42 | versions = service.getValueSetVersionsByValueSetOid(vsoid) 43 | vsid = sorted(versions.valueSetVersions, key=lambda x: x.versionNumber)[-1].id 44 | 45 | value_set_concepts.remove({'valueSetOid':vsoid}) 46 | def fetchPage(pnum): 47 | return service.getValueSetConceptsByValueSetVersionId(vsid, pnum, PAGESIZE) 48 | 49 | pnum = 1 # 1-index pages 50 | fetched = 0 51 | 52 | while True: 53 | p = fetchPage(pnum) 54 | pnum += 1 55 | 56 | for r in p.valueSetConcepts: 57 | cs = getCodeSystem(r.codeSystemOid) 58 | rbson = toBSON(r) 59 | rbson['codeSystemName'] = cs['name'] 60 | rbson['valueSetOid'] = vsoid 61 | value_set_concepts.insert(rbson) 62 | 63 | fetched += len(p.valueSetConcepts) 64 | print "fetched", fetched 65 | if fetched >= p.totalResults: 66 | break 67 | 68 | if __name__ == "__main__": 69 | import json 70 | s = open("phinvads_valueset_oids.json").read() 71 | s = json.loads(s) 72 | for k in s: 73 | print "Fetching", s[k] 74 | fetchValueSet(k) 75 | -------------------------------------------------------------------------------- /value_sets/mustaine/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010, Brandon Gilmore 2 | # Copyright (c) 2010, Phill Tornroth 3 | # All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are 7 | # met: 8 | # 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # * Neither the name of the software nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 22 | # OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | version_info = (0, 1, 7) 31 | __version__ = ".".join(map(str, version_info)) 32 | 33 | -------------------------------------------------------------------------------- /value_sets/mustaine/_util.py: -------------------------------------------------------------------------------- 1 | try: 2 | from cStringIO import StringIO 3 | except ImportError: 4 | from StringIO import StringIO 5 | 6 | class BufferedReader(object): 7 | 8 | def __init__(self, input, buffer_size=65535): 9 | self.__input = input 10 | self.__buffer_size = buffer_size 11 | self.__buffer = StringIO() 12 | 13 | # initial fill 14 | chunk = input.read(buffer_size) 15 | self.__byte_count = len(chunk) 16 | self.__buffer.write(chunk) 17 | self.__buffer.seek(0) 18 | 19 | def read(self, byte_count): 20 | difference = byte_count - self.__byte_count 21 | 22 | if difference < 0: 23 | chunk = self.__buffer.read(byte_count) 24 | self.__byte_count -= byte_count 25 | else: 26 | chunk = self.__buffer.read() + self.__input.read(difference) 27 | 28 | # verify size 29 | if len(chunk) != byte_count: 30 | raise EOFError("Encountered unexpected end of stream") 31 | 32 | # reset internal buffer 33 | self.__buffer.seek(0) 34 | self.__buffer.truncate() 35 | 36 | # replenish 37 | fresh_chunk = self.__input.read(self.__buffer_size) 38 | self.__byte_count = len(fresh_chunk) 39 | self.__buffer.write(fresh_chunk) 40 | self.__buffer.seek(0) 41 | 42 | return chunk 43 | 44 | -------------------------------------------------------------------------------- /value_sets/mustaine/client.py: -------------------------------------------------------------------------------- 1 | from httplib import HTTPConnection, HTTPSConnection 2 | from urlparse import urlparse 3 | from warnings import warn 4 | import base64 5 | import sys 6 | 7 | from mustaine.encoder import encode_object 8 | from mustaine.parser import Parser 9 | from mustaine.protocol import Call, Fault 10 | from mustaine._util import BufferedReader 11 | from mustaine import __version__ 12 | 13 | 14 | class ProtocolError(Exception): 15 | """ Raised when an HTTP error occurs """ 16 | def __init__(self, url, status, reason): 17 | self._url = url 18 | self._status = status 19 | self._reason = reason 20 | 21 | def __str__(self): 22 | return self.__repr__() 23 | 24 | def __repr__(self): 25 | return "" % (self._url, self._status, self._reason,) 26 | 27 | 28 | class HessianProxy(object): 29 | 30 | def __init__(self, service_uri, credentials=None, key_file=None, cert_file=None, timeout=10, buffer_size=65535, error_factory=lambda x: x, overload=False): 31 | self._headers = list() 32 | self._headers.append(('User-Agent', 'mustaine/' + __version__,)) 33 | self._headers.append(('Content-Type', 'application/x-hessian',)) 34 | 35 | if sys.version_info < (2,6): 36 | warn('HessianProxy timeout not enforceable before Python 2.6', RuntimeWarning, stacklevel=2) 37 | timeout = {} 38 | else: 39 | timeout = {'timeout': timeout} 40 | 41 | self._uri = urlparse(service_uri) 42 | if self._uri.scheme == 'http': 43 | self._client = HTTPConnection(self._uri.hostname, 44 | self._uri.port or 80, 45 | strict=True, 46 | **timeout) 47 | elif self._uri.scheme == 'https': 48 | self._client = HTTPSConnection(self._uri.hostname, 49 | self._uri.port or 443, 50 | key_file=key_file, 51 | cert_file=cert_file, 52 | strict=True, 53 | **timeout) 54 | else: 55 | raise NotImplementedError("HessianProxy only supports http:// and https:// URIs") 56 | 57 | # autofill credentials if they were passed via url instead of kwargs 58 | if (self._uri.username and self._uri.password) and not credentials: 59 | credentials = (self._uri.username, self._uri.password) 60 | 61 | if credentials: 62 | auth = 'Basic ' + base64.b64encode(':'.join(credentials)) 63 | self._headers.append(('Authorization', auth)) 64 | 65 | self._buffer_size = buffer_size 66 | self._error_factory = error_factory 67 | self._overload = overload 68 | self._parser = Parser() 69 | 70 | class __RemoteMethod(object): 71 | # dark magic for autoloading methods 72 | def __init__(self, caller, method): 73 | self.__caller = caller 74 | self.__method = method 75 | def __call__(self, *args): 76 | return self.__caller(self.__method, args) 77 | 78 | def __getattr__(self, method): 79 | return self.__RemoteMethod(self, method) 80 | 81 | def __repr__(self): 82 | return "" % (self._uri.geturl(),) 83 | 84 | def __str__(self): 85 | return self.__repr__() 86 | 87 | def __call__(self, method, args): 88 | try: 89 | self._client.putrequest('POST', self._uri.path) 90 | for header in self._headers: 91 | self._client.putheader(*header) 92 | 93 | request = encode_object(Call(method, args, overload=self._overload)) 94 | self._client.putheader("Content-Length", str(len(request))) 95 | self._client.endheaders() 96 | self._client.send(str(request)) 97 | 98 | response = self._client.getresponse() 99 | if response.status != 200: 100 | raise ProtocolError(self._uri.geturl(), response.status, response.reason) 101 | 102 | length = response.getheader('Content-Length', -1) 103 | if length == '0': 104 | raise ProtocolError(self._uri.geturl(), 'FATAL:', 'Server sent zero-length response') 105 | 106 | reply = self._parser.parse_stream(BufferedReader(response, buffer_size=self._buffer_size)) 107 | self._client.close() 108 | 109 | if isinstance(reply.value, Fault): 110 | raise self._error_factory(reply.value) 111 | else: 112 | return reply.value 113 | except: 114 | self._client.close() 115 | raise 116 | -------------------------------------------------------------------------------- /value_sets/mustaine/encoder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | from struct import pack 4 | 5 | from types import * 6 | from mustaine.protocol import * 7 | 8 | # Implementation of Hessian 1.0.2 serialization 9 | # see: http://hessian.caucho.com/doc/hessian-1.0-spec.xtp 10 | 11 | ENCODERS = {} 12 | def encoder_for(data_type): 13 | def register(f): 14 | # register function `f` to encode type `data_type` 15 | ENCODERS[data_type] = f 16 | return f 17 | return register 18 | 19 | def returns(data_type): 20 | def wrap(f): 21 | # wrap function `f` to return a tuple of (type,data) 22 | def wrapped(*args): 23 | return data_type, f(*args) 24 | return wrapped 25 | return wrap 26 | 27 | def encode_object(obj): 28 | if type(obj) in ENCODERS: 29 | encoder = ENCODERS[type(obj)] 30 | else: 31 | raise TypeError("mustaine.encoder cannot serialize %s" % (type(obj),)) 32 | 33 | return encoder(obj)[1] 34 | 35 | 36 | @encoder_for(NoneType) 37 | @returns('null') 38 | def encode_null(_): 39 | return 'N' 40 | 41 | @encoder_for(BooleanType) 42 | @returns('bool') 43 | def encode_boolean(value): 44 | if value: 45 | return 'T' 46 | else: 47 | return 'F' 48 | 49 | @encoder_for(IntType) 50 | @returns('int') 51 | def encode_int(value): 52 | return pack('>cl', 'I', value) 53 | 54 | @encoder_for(LongType) 55 | @returns('long') 56 | def encode_long(value): 57 | return pack('>cq', 'L', value) 58 | 59 | @encoder_for(FloatType) 60 | @returns('double') 61 | def encode_double(value): 62 | return pack('>cd', 'D', value) 63 | 64 | @encoder_for(datetime.datetime) 65 | @returns('date') 66 | def encode_date(value): 67 | return pack('>cq', 'd', int(time.mktime(value.timetuple())) * 1000) 68 | 69 | @encoder_for(StringType) 70 | @returns('string') 71 | def encode_string(value): 72 | encoded = '' 73 | 74 | try: 75 | value = value.encode('ascii') 76 | except UnicodeDecodeError: 77 | raise TypeError("mustaine.encoder cowardly refuses to guess the encoding for " 78 | "string objects containing bytes out of range 0x00-0x79; use " 79 | "Binary or unicode objects instead") 80 | 81 | while len(value) > 65535: 82 | encoded += pack('>cH', 's', 65535) 83 | encoded += value[:65535] 84 | value = value[65535:] 85 | 86 | encoded += pack('>cH', 'S', len(value.decode('utf-8'))) 87 | encoded += value 88 | return encoded 89 | 90 | @encoder_for(UnicodeType) 91 | @returns('string') 92 | def encode_unicode(value): 93 | encoded = '' 94 | 95 | while len(value) > 65535: 96 | encoded += pack('>cH', 's', 65535) 97 | encoded += value[:65535].encode('utf-8') 98 | value = value[65535:] 99 | 100 | encoded += pack('>cH', 'S', len(value)) 101 | encoded += value.encode('utf-8') 102 | return encoded 103 | 104 | @encoder_for(ListType) 105 | @returns('list') 106 | def encode_list(obj): 107 | encoded = ''.join(map(encode_object, obj)) 108 | return pack('>2cl', 'V', 'l', -1) + encoded + 'z' 109 | 110 | @encoder_for(TupleType) 111 | @returns('list') 112 | def encode_tuple(obj): 113 | encoded = ''.join(map(encode_object, obj)) 114 | return pack('>2cl', 'V', 'l', len(obj)) + encoded + 'z' 115 | 116 | def encode_keyval(pair): 117 | return ''.join((encode_object(pair[0]), encode_object(pair[1]))) 118 | 119 | @encoder_for(DictType) 120 | @returns('map') 121 | def encode_map(obj): 122 | encoded = ''.join(map(encode_keyval, obj.items())) 123 | return pack('>c', 'M') + encoded + 'z' 124 | 125 | @encoder_for(Object) 126 | def encode_mobject(obj): 127 | encoded = pack('>cH', 't', len(obj._meta_type)) + obj._meta_type 128 | members = obj.__getstate__() 129 | del members['__meta_type'] # this is here for pickling. we don't want or need it 130 | 131 | encoded += ''.join(map(encode_keyval, members.items())) 132 | return (obj._meta_type.rpartition('.')[2], pack('>c', 'M') + encoded + 'z') 133 | 134 | @encoder_for(Remote) 135 | @returns('remote') 136 | def encode_remote(obj): 137 | encoded = encode_string(obj.url) 138 | return pack('>2cH', 'r', 't', len(obj.type_name)) + obj.type_name + encoded 139 | 140 | @encoder_for(Binary) 141 | @returns('binary') 142 | def encode_binary(obj): 143 | encoded = '' 144 | value = obj.value 145 | 146 | while len(value) > 65535: 147 | encoded += pack('>cH', 'b', 65535) 148 | encoded += value[:65535] 149 | value = value[65535:] 150 | 151 | encoded += pack('>cH', 'B', len(value)) 152 | encoded += value 153 | 154 | return encoded 155 | 156 | @encoder_for(Call) 157 | @returns('call') 158 | def encode_call(call): 159 | method = call.method 160 | headers = '' 161 | arguments = '' 162 | 163 | for header,value in call.headers.items(): 164 | if not isinstance(header, StringType): 165 | raise TypeError("Call header keys must be strings") 166 | 167 | headers += pack('>cH', 'H', len(header)) + header 168 | headers += encode_object(value) 169 | 170 | # TODO: this is mostly duplicated at the top of the file in encode_object. dedup 171 | for arg in call.args: 172 | if type(arg) in ENCODERS: 173 | encoder = ENCODERS[type(arg)] 174 | else: 175 | raise TypeError("mustaine.encoder cannot serialize %s" % (type(arg),)) 176 | 177 | data_type, arg = encoder(arg) 178 | if call.overload: 179 | method += '_' + data_type 180 | arguments += arg 181 | 182 | encoded = pack('>cBB', 'c', 1, 0) 183 | encoded += headers 184 | encoded += pack('>cH', 'm', len(method)) + method 185 | encoded += arguments 186 | encoded += 'z' 187 | 188 | return encoded 189 | 190 | -------------------------------------------------------------------------------- /value_sets/mustaine/parser.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from struct import unpack 3 | 4 | try: 5 | from cStringIO import StringIO 6 | except ImportError: 7 | from StringIO import StringIO 8 | 9 | from mustaine.protocol import * 10 | 11 | # Implementation of Hessian 1.0.2 deserialization 12 | # see: http://hessian.caucho.com/doc/hessian-1.0-spec.xtp 13 | 14 | class ParseError(Exception): 15 | pass 16 | 17 | class Parser(object): 18 | def parse_string(self, string): 19 | if isinstance(string, unicode): 20 | stream = StringIO(string.encode('utf-8')) 21 | else: 22 | stream = StringIO(string) 23 | 24 | return self.parse_stream(stream) 25 | 26 | def parse_stream(self, stream): 27 | self._refs = [] 28 | self._result = None 29 | 30 | if hasattr(stream, 'read') and hasattr(stream.read, '__call__'): 31 | self._stream = stream 32 | else: 33 | raise TypeError('Stream parser can only handle objects supporting read()') 34 | 35 | while True: 36 | code = self._read(1) 37 | 38 | if code == 'c': 39 | if self._result: 40 | raise ParseError('Encountered duplicate type header') 41 | 42 | version = self._read(2) 43 | if version != '\x01\x00': 44 | raise ParseError("Encountered unrecognized call version %r" % (version,)) 45 | 46 | self._result = Call() 47 | continue 48 | 49 | elif code == 'r': 50 | if self._result: 51 | raise ParseError('Encountered duplicate type header') 52 | 53 | version = self._read(2) 54 | if version != '\x01\x00': 55 | raise ParseError("Encountered unrecognized reply version %r" % (version,)) 56 | 57 | self._result = Reply() 58 | continue 59 | 60 | else: 61 | if not self._result: 62 | raise ParseError("Invalid Hessian message marker: %r" % (code,)) 63 | 64 | if code == 'H': 65 | key, value = self._read_keyval() 66 | self._result.headers[key] = value 67 | continue 68 | 69 | elif code == 'm': 70 | if not isinstance(self._result, Call): 71 | raise ParseError('Encountered illegal method name within reply') 72 | 73 | if self._result.method: 74 | raise ParseError('Encountered duplicate method name definition') 75 | 76 | self._result.method = self._read(unpack('>H', self._read(2))[0]) 77 | continue 78 | 79 | elif code == 'f': 80 | if not isinstance(self._result, Reply): 81 | raise ParseError('Encountered illegal fault within call') 82 | 83 | if self._result.value: 84 | raise ParseError('Encountered illegal extra object within reply') 85 | 86 | self._result.value = self._read_fault() 87 | continue 88 | 89 | elif code == 'z': 90 | break 91 | 92 | else: 93 | if isinstance(self._result, Call): 94 | self._result.args.append(self._read_object(code)) 95 | else: 96 | if self._result.value: 97 | raise ParseError('Encountered illegal extra object within reply') 98 | 99 | self._result.value = self._read_object(code) 100 | 101 | # have to hit a 'z' to land here, TODO derefs? 102 | return self._result 103 | 104 | 105 | def _read(self, n): 106 | try: 107 | r = self._stream.read(n) 108 | except IOError: 109 | raise ParseError('Encountered unexpected end of stream') 110 | except: 111 | raise 112 | else: 113 | if len(r) == 0: 114 | raise ParseError('Encountered unexpected end of stream') 115 | 116 | return r 117 | 118 | def _read_object(self, code): 119 | if code == 'N': 120 | return None 121 | elif code == 'T': 122 | return True 123 | elif code == 'F': 124 | return False 125 | elif code == 'I': 126 | return int(unpack('>l', self._read(4))[0]) 127 | elif code == 'L': 128 | return long(unpack('>q', self._read(8))[0]) 129 | elif code == 'D': 130 | return float(unpack('>d', self._read(8))[0]) 131 | elif code == 'd': 132 | return self._read_date() 133 | elif code == 's' or code == 'x': 134 | fragment = self._read_string() 135 | next = self._read(1) 136 | if next.lower() == code: 137 | return fragment + self._read_object(next) 138 | else: 139 | raise ParseError("Expected terminal string segment, got %r" % (next,)) 140 | elif code == 'S' or code == 'X': 141 | return self._read_string() 142 | elif code == 'b': 143 | fragment = self._read_binary() 144 | next = self._read(1) 145 | if next.lower() == code: 146 | return fragment + self._read_object(next) 147 | else: 148 | raise ParseError("Expected terminal binary segment, got %r" % (next,)) 149 | elif code == 'B': 150 | return self._read_binary() 151 | elif code == 'r': 152 | return self._read_remote() 153 | elif code == 'R': 154 | return self._refs[unpack(">L", self._read(4))[0]] 155 | elif code == 'V': 156 | return self._read_list() 157 | elif code == 'M': 158 | return self._read_map() 159 | else: 160 | raise ParseError("Unknown type marker %r" % (code,)) 161 | 162 | def _read_date(self): 163 | timestamp = unpack('>q', self._read(8))[0] 164 | return datetime.datetime.fromtimestamp(timestamp / 1000) 165 | 166 | def _read_string(self): 167 | len = unpack('>H', self._read(2))[0] 168 | 169 | bytes = [] 170 | while len > 0: 171 | byte = self._read(1) 172 | if ord(byte) in range(0x00, 0x80): 173 | bytes.append(byte) 174 | elif ord(byte) in range(0xC2, 0xE0): 175 | bytes.append(byte + self._read(1)) 176 | elif ord(byte) in range(0xE0, 0xF0): 177 | bytes.append(byte + self._read(2)) 178 | elif ord(byte) in range(0xF0, 0xF5): 179 | bytes.append(byte + self._read(3)) 180 | len -= 1 181 | 182 | return ''.join(bytes).decode('utf-8') 183 | 184 | def _read_binary(self): 185 | len = unpack('>H', self._read(2))[0] 186 | return Binary(self._read(len)) 187 | 188 | def _read_remote(self): 189 | r = Remote() 190 | code = self._read(1) 191 | 192 | if code == 't': 193 | r.type = self._read(unpack('>H', self._read(2))[0]) 194 | code = self._read(1) 195 | else: 196 | r.type = None 197 | 198 | if code != 's' and code != 'S': 199 | raise ParseError("Expected string object while parsing Remote object URL") 200 | 201 | r.url = self._read_object(code) 202 | return r 203 | 204 | def _read_list(self): 205 | code = self._read(1) 206 | 207 | if code == 't': 208 | # read and discard list type 209 | self._read(unpack('>H', self._read(2))[0]) 210 | code = self._read(1) 211 | 212 | if code == 'l': 213 | # read and discard list length 214 | self._read(4) 215 | code = self._read(1) 216 | 217 | result = [] 218 | self._refs.append(result) 219 | 220 | while code != 'z': 221 | result.append(self._read_object(code)) 222 | code = self._read(1) 223 | 224 | return result 225 | 226 | def _read_map(self): 227 | code = self._read(1) 228 | 229 | if code == 't': 230 | type_len = unpack('>H', self._read(2))[0] 231 | if type_len > 0: 232 | # a typed map deserializes to an object 233 | result = Object(self._read(type_len)) 234 | else: 235 | result = {} 236 | 237 | code = self._read(1) 238 | else: 239 | # untyped maps deserialize to a dict 240 | result = {} 241 | 242 | self._refs.append(result) 243 | 244 | fields = {} 245 | while code != 'z': 246 | key, value = self._read_keyval(code) 247 | 248 | if isinstance(result, Object): 249 | fields[str(key)] = value 250 | else: 251 | fields[key] = value 252 | 253 | code = self._read(1) 254 | 255 | if isinstance(result, Object): 256 | fields['__meta_type'] = result._meta_type 257 | result.__setstate__(fields) 258 | else: 259 | result.update(fields) 260 | 261 | return result 262 | 263 | def _read_fault(self): 264 | fault = self._read_map() 265 | return Fault(fault['code'], fault['message'], fault.get('detail')) 266 | 267 | def _read_keyval(self, first=None): 268 | key = self._read_object(first or self._read(1)) 269 | value = self._read_object(self._read(1)) 270 | 271 | return key, value 272 | 273 | -------------------------------------------------------------------------------- /value_sets/mustaine/protocol.py: -------------------------------------------------------------------------------- 1 | # transparent types used for hessian serialization 2 | # objects of this type can appear on the wire but have no native python type 3 | 4 | class Call(object): 5 | def __init__(self, method=None, args=None, headers=None, overload=None): 6 | self._method = method or '' 7 | self._args = args or list() 8 | self._headers = headers or dict() 9 | self._overload = overload or False 10 | 11 | def _get_method(self): 12 | return self._method 13 | 14 | def _set_method(self, value): 15 | if isinstance(value, str): 16 | self._method = value 17 | else: 18 | raise TypeError("Call.method must be a string") 19 | 20 | method = property(_get_method, _set_method) 21 | 22 | def _get_args(self): 23 | return self._args 24 | 25 | def _set_args(self, value): 26 | if hasattr(value, '__iter__'): 27 | self._args = value 28 | else: 29 | raise TypeError("Call.args must be an iterable value") 30 | 31 | args = property(_get_args, _set_args) 32 | 33 | def _get_headers(self): 34 | return self._headers 35 | 36 | def _set_headers(self, value): 37 | if not isinstance(value, dict): 38 | raise TypeError("Call.headers must be a dict of strings to objects") 39 | 40 | for key in value.keys(): 41 | if not isinstance(key, basestring): 42 | raise TypeError("Call.headers must be a dict of strings to objects") 43 | 44 | self._headers = value 45 | 46 | headers = property(_get_headers, _set_headers) 47 | 48 | def _get_overload(self): 49 | return self._overload 50 | 51 | def _set_overload(self, value): 52 | if isinstance(value, bool): 53 | self._overload = value 54 | else: 55 | raise TypeError("Call.overload must be True or False") 56 | 57 | overload = property(_get_overload, _set_overload) 58 | 59 | 60 | class Reply(object): 61 | def __init__(self, value=None, headers=None): 62 | self.value = value # unmanaged property 63 | self._headers = headers or dict() 64 | 65 | def _get_headers(self): 66 | return self._headers 67 | 68 | def _set_headers(self, value): 69 | if not isinstance(value, dict): 70 | raise TypeError("Call.headers must be a dict of strings to objects") 71 | 72 | for key in value.keys(): 73 | if not isinstance(key, basestring): 74 | raise TypeError("Call.headers must be a dict of strings to objects") 75 | 76 | self._headers = value 77 | 78 | headers = property(_get_headers, _set_headers) 79 | 80 | 81 | class Fault(Exception): 82 | def __init__(self, code, message, detail): 83 | self.code = code 84 | self.message = message 85 | self.detail = detail 86 | 87 | # 'message' property implemented to mask DeprecationWarning 88 | def _get_message(self): 89 | return self.__message 90 | 91 | def _set_message(self, message): 92 | self.__message = message 93 | 94 | message = property(_get_message, _set_message) 95 | 96 | def __repr__(self): 97 | return "" % (self.code, self.message,) 98 | 99 | def __str__(self): 100 | return self.__repr__() 101 | 102 | 103 | class Binary(object): 104 | def __init__(self, value): 105 | self.value = value 106 | def __add__(self, value): 107 | if self.value == None: 108 | return Binary(value) 109 | else: 110 | return Binary(self.value + value.value) 111 | 112 | 113 | class Remote(object): 114 | def __init__(self, type_name=None, url=None): 115 | self.type_name = type_name 116 | self.url = url 117 | 118 | 119 | class Object(object): 120 | def __init__(self, meta_type, **kwargs): 121 | self.__meta_type = meta_type 122 | 123 | if kwargs: 124 | for key in kwargs: 125 | self.__dict__[key] = kwargs[key] 126 | 127 | @property 128 | def _meta_type(self): 129 | return self.__meta_type 130 | 131 | def __repr__(self): 132 | return "<%s object at %s>" % (self.__meta_type, hex(id(self)),) 133 | 134 | def __getstate__(self): 135 | # clear metadata for clean pickling 136 | t = self.__meta_type 137 | del self.__meta_type 138 | 139 | d = self.__dict__.copy() 140 | d['__meta_type'] = t 141 | 142 | # restore metadata 143 | self.__meta_type = t 144 | 145 | return d 146 | 147 | def __setstate__(self, d): 148 | self.__meta_type = d['__meta_type'] 149 | del d['__meta_type'] 150 | 151 | self.__dict__.update(d) 152 | 153 | -------------------------------------------------------------------------------- /value_sets/other_valuesets.txt: -------------------------------------------------------------------------------- 1 | Value Set: HL7 BasicConfidentialityKind 2.16.840.1.113883.1.11.16926 STATIC 2010-04-21 2 | Code System(s): Confidentiality Code 2.16.840.1.113883.5.25 3 | Code Code System Print Name 4 | N Confidentiality Code Normal 5 | R Confidentiality Code Restricted 6 | V Confidentiality Code Very Restricted 7 | 8 | Value Set: Telecom Use (US Realm Header) 2.16.840.1.113883.11.20.9.20 DYNAMIC 9 | Code System(s): AddressUse 2.16.840.1.113883.5.1119 10 | Code Code System Print Name 11 | HP AddressUse primary home 12 | WP AddressUse work place 13 | MC AddressUse mobile contact 14 | HV AddressUse vacation home 15 | 16 | Value Set: LanguageAbilityProficiency 2.16.840.1.113883.1.11.12199 DYNAMIC 17 | Code System(s): LanguageAbilityProficiency 2.16.840.1.113883.5.61 18 | Description: 19 | A value representing the level of proficiency in a language. 20 | Code Code System Print Name 21 | E LanguageAbilityProficiency Excellent 22 | F LanguageAbilityProficiency Fair 23 | G LanguageAbilityProficiency Good 24 | P LanguageAbilityProficiency Poor 25 | 26 | Value Set: INDRoleclassCodes 2.16.840.1.113883.11.20.9.33 STATIC 2011-09-30 27 | Code System(s): 28 | Code 29 | RoleClass 2.16.840.1.113883.5.110 30 | Code System 31 | Print Name 32 | PRS RoleClass personal relationship 33 | NOK RoleClass next of kin 34 | CAREGIVER RoleClass caregiver 35 | AGNT RoleClass agent 36 | GUAR RoleClass guarantor 37 | ECON RoleClass emergency contact 38 | 39 | Value Set: EntityNameUse 2.16.840.1.113883.1.11.15913 STATIC 2005-05-01 40 | Code System(s): 41 | EntityNameUse 2.16.840.1.113883.5.45 42 | Code Code System Print Name 43 | A EntityNameUse Artist/Stage 44 | ABC EntityNameUse Alphabetic 45 | ASGN EntityNameUse Assigned 46 | C EntityNameUse License 47 | I EntityNameUse Indigenous/Tribal 48 | IDE EntityNameUse Ideographic 49 | L EntityNameUse Legal 50 | P EntityNameUse Pseudonym 51 | PHON EntityNameUse Phonetic 52 | R EntityNameUse Religious 53 | SNDX EntityNameUse Soundex 54 | SRCH EntityNameUse Search 55 | SYL EntityNameUse Syllabic 56 | 57 | Value Set: EntityPersonNamePartQualifier 2.16.840.1.113883.11.20.9.26 STATIC 58 | 2011-09-30 59 | Code System(s): 60 | Code 61 | EntityNamePartQualifier 2.16.840.1.113883.5.43 62 | Code System 63 | Print Name 64 | AC academic 65 | AD EntityNamePartQualifier adopted 66 | BR EntityNamePartQualifier birth 67 | CL EntityNamePartQualifier callme 68 | IN EntityNamePartQualifier initial 69 | NB EntityNamePartQualifier nobility 70 | PR EntityNamePartQualifier professional 71 | SP EntityNamePartQualifier spouse 72 | TITLE EntityNamePartQualifier title 73 | VV EntityNamePartQualifier voorvoegsel 74 | 75 | Value Set: ProblemAct statusCode 2.16.840.1.113883.11.20.9.19 STATIC 2011-09-09 76 | Code System(s): ActStatus 2.16.840.1.113883.5.14 77 | Description: This value set indicates the status of the problem concern act 78 | Code Code System Print Name 79 | active ActStatus active 80 | suspended ActStatus suspended 81 | aborted ActStatus aborted 82 | completed ActStatus completed 83 | 84 | Value Set: AgePQ_UCUM 2.16.840.1.113883.11.20.9.21 DYNAMIC 85 | Code System(s): Unified Code for Units of Measure (UCUM) 2.16.840.1.113883.6.8 86 | Description: A valueSet of UCUM codes for representing age value units 87 | Code Code System Print Name 88 | min UCUM Minute 89 | h UCUM Hour 90 | d UCUM Day 91 | wk UCUM Week 92 | mo UCUM Month 93 | a UCUM Year 94 | 95 | 96 | Value Set: HITSPProblemStatus 2.16.840.1.113883.3.88.12.80.68 DYNAMIC 97 | Code System: SNOMED CT 2.16.840.1.113883.6.96 98 | Code Code System Display Name 99 | 55561003 SNOMED CT Active 100 | 73425007 SNOMED CT Inactive* 101 | 413322009 SNOMED CT Resolved** 102 | 103 | Value Set: HealthStatus 2.16.840.1.113883.1.11.20.12 DYNAMIC 104 | Code System(s): 105 | Description: 106 | Code 107 | SNOMED CT 2.16.840.1.113883.6.96 108 | Represents the general health status of the patient. 109 | Code System Print Name 110 | 81323004 SNOMED CT Alive and well 111 | 313386006 SNOMED CT In remission 112 | 162467007 SNOMED CT Symptom free 113 | 161901003 SNOMED CT Chronically ill 114 | 271593001 SNOMED CT Severely ill 115 | 21134002 SNOMED CT Disabled 116 | 161045001 SNOMED CT Severely disabled 117 | 118 | 119 | Value Set: Medication Fill Status 2.16.840.1.113883.3.88.12.80.64 DYNAMIC 120 | Code System: ActStatus 2.16.840.1.113883.5.14 121 | Code Code System Print Name 122 | aborted ActStatus Aborted 123 | completed ActStatus Completed 124 | 125 | SNOMED CT Subsumptions: 126 | 2.16.840.1.113883.11.20.9.34 : "Patient education Value Set" << Education (409073007) 127 | 128 | -------------------------------------------------------------------------------- /value_sets/phinvads_valueset_oids.json: -------------------------------------------------------------------------------- 1 | { 2 | "2.16.840.1.113883.1.11.1": "Administrative Gender", 3 | "2.16.840.1.113883.1.11.12212": "Marital Status", 4 | "2.16.840.1.113883.1.11.19185": "Religious Affiliation", 5 | "2.16.840.1.113883.1.11.14914": "Race", 6 | "2.16.840.1.114222.4.11.837": "Ethnicity", 7 | "2.16.840.1.113883.1.11.19563": "Personal Relationship Role", 8 | "2.16.840.1.113883.1.11.12249": "Language Ability Mode", 9 | "2.16.840.1.113883.3.88.12.3221.7.2": "Problem Type", 10 | "2.16.840.1.113883.3.88.12.3221.7.4": "Problem", 11 | "2.16.840.1.113883.3.88.12.3221.8.7": "Medication Route", 12 | "2.16.840.1.113883.3.88.12.3221.8.9": "Body Site", 13 | "2.16.840.1.113883.3.88.12.80.17": "Clinical Drug", 14 | "2.16.840.1.113883.3.88.12.80.18": "Drug Class", 15 | "2.16.840.1.113883.3.88.12.3221.8.11": "Medication Product Form", 16 | "2.16.840.1.113883.3.88.12.3221.6.8": "Problem Secerity", 17 | "2.16.840.1.113883.3.88.12.3221.6.2": " Allergy or Adverse Event Type" 18 | } 19 | -------------------------------------------------------------------------------- /value_sets/rxnorm/genlinks.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sqlite3 3 | from pymongo import Connection 4 | 5 | connection = Connection() 6 | db = connection.rxnorm 7 | 8 | conn = sqlite3.connect('rxn.db') 9 | 10 | def doQ(q): 11 | ret= [x[0] for x in conn.execute(q).fetchall()] 12 | return ret 13 | 14 | def instr(l): 15 | return "('"+"','".join(l) +"')" 16 | 17 | def toBrandAndGeneric(rxcuis, tty): 18 | ret = [] 19 | for rxcui in rxcuis: 20 | ret.extend(doQ("select rxcui1 from rxnrel where rela='tradename_of' and rxcui2='%s' """%rxcui)) 21 | return ret 22 | 23 | def toComponents(rxcuis, tty): 24 | ret = [] 25 | 26 | if tty not in ("SBD", "SCD"): 27 | return ret 28 | 29 | for rxcui in rxcuis: 30 | cs = doQ("select rxcui1 from rxnrel where rela='consists_of' and rxcui2='%s' """%rxcui) 31 | for c in cs: 32 | ret.extend(doQ("select rxcui from rxnconso where sab='RXNORM' and tty='SCDC' and rxcui='%s'"%c)) 33 | 34 | return set(ret) 35 | 36 | def toTreatmentIntents(rxcuis, tty): 37 | ret = [] 38 | for v in rxcuis: 39 | ret.extend(toTreatmentIntents_helper(v, tty)) 40 | return set(ret) 41 | 42 | def toTreatmentIntents_helper(rxcui, tty): 43 | assert tty=='IN' 44 | ret = [] 45 | rxauis = doQ("select rxaui from rxnconso where tty='FN' and sab='NDFRT' and rxcui='%s'"%rxcui) 46 | for a in rxauis: 47 | a1 = doQ("select rxaui1 from rxnrel where rxaui2='%s' and rela='may_treat'"%a) 48 | if len(a1)>0: 49 | dz = doQ("select str from rxnconso c where c.tty='FN' and c.sab='NDFRT' and rxaui='%s'"%a1[0]) 50 | dz = map(lambda x: x.replace(" [Disease/Finding]", ""), dz) 51 | ret.extend(dz) 52 | return ret 53 | 54 | def toMechanism(rxcuis, tty): 55 | ret = [] 56 | for v in rxcuis: 57 | ret.extend(toMechanism_helper(v, tty)) 58 | return set(ret) 59 | 60 | def toMechanism_helper(rxcui, tty): 61 | assert tty=='IN' 62 | ret = [] 63 | rxauis = doQ("select rxaui from rxnconso where tty='FN' and sab='NDFRT' and rxcui='%s'"%rxcui) 64 | for a in rxauis: 65 | a1 = doQ("select rxaui1 from rxnrel where rxaui2='%s' and rela='has_mechanism_of_action'"%a) 66 | if len(a1)>0: 67 | moa = doQ("select str from rxnconso c where c.tty='FN' and c.sab='NDFRT' and rxaui='%s'"%a1[0]) 68 | moa = map(lambda x: x.replace(" [MoA]", ""), moa) 69 | ret.extend(moa) 70 | return ret 71 | 72 | 73 | def toIngredients(rxcuis, tty): 74 | ret = [] 75 | for v in rxcuis: 76 | ret.extend(toIngredients_helper(v, tty)) 77 | return set(ret) 78 | 79 | def toIngredients_helper(rxcui, tty): 80 | if tty=='IN': return [rxcui] 81 | 82 | if tty=='MIN': 83 | return doQ("select rxcui1 from rxnrel where rxcui2 ='%s' and rela='has_part'"%rxcui) 84 | 85 | if tty=='PIN': 86 | return doQ("select rxcui1 from rxnrel where rxcui2 ='%s' and rela='form_of'"%rxcui) 87 | 88 | if tty=='BN': 89 | return doQ("select rxcui1 from rxnrel where rela='tradename_of' and rxcui2='%s' """%rxcui) 90 | 91 | if tty=='SCDF': 92 | return doQ("select rxcui1 from rxnrel where rxcui2 ='%s' and rela='has_ingredient'"%rxcui) 93 | 94 | if tty=='SBDF': 95 | return toIngredients(doQ("select rxcui1 from rxnrel where rxcui2='%s' and rela='tradename_of'"%rxcui), 'SCDF') 96 | 97 | if tty=='SCDG': 98 | return doQ("select rxcui1 from rxnrel where rxcui2 ='%s' and rela='has_ingredient'"%rxcui) 99 | 100 | if tty=='SBDG': 101 | return toIngredients(doQ("select rxcui1 from rxnrel where rxcui2='%s' and rela='tradename_of'"%rxcui), 'SCDG') 102 | 103 | if tty=='SBDC': 104 | return toIngredients(doQ("select rxcui1 from rxnrel where rxcui2='%s' and rela='tradename_of'"%rxcui), 'SCDC') 105 | 106 | if tty=='SCDC': 107 | return doQ("select rxcui1 from rxnrel where rxcui2 ='%s' and rela='has_ingredient'"%rxcui) 108 | 109 | if tty=='SBD': 110 | return toIngredients(doQ("select rxcui1 from rxnrel where rxcui2='%s' and rela='tradename_of'"%rxcui), 'SCD') 111 | 112 | if tty=='SCD': 113 | return toIngredients(doQ("select rxcui1 from rxnrel where rxcui2='%s' and rela='consists_of'"%rxcui), 'SCDC') 114 | 115 | if tty=='BPCK' or tty=='GPCK': 116 | return toIngredients(doQ("select rxcui1 from rxnrel where rxcui2='%s' and rela='contains'"%rxcui), 'SCD') 117 | 118 | """ 119 | print toIngredients(['369070'], 'SBDF') 120 | print toIngredients(['901813'], 'SCDC') 121 | print toIngredients(['209459'], 'SBD') 122 | print toIngredients(['214181'], 'MIN') 123 | print toIngredients(['203150'], 'PIN') 124 | print toIngredients(['58930'], 'BN') 125 | print toIngredients(['1092412'], 'BPCK') 126 | print toIngredients(['1093075'], 'SCD') 127 | 128 | OCD Obsolete clinical drug 295942 129 | SY Designated synonym 47804 130 | SCD Semantic Clinical Drug 33725 131 | SCDC Semantic Drug Component 25774 132 | TMSY Tall Man synonym 22764 133 | SBDG Semantic branded drug group 22286 134 | SBD Semantic branded drug 21011 135 | SBDC Semantic Branded Drug Component 18885 136 | BN Fully-specified drug brand name that can not be prescribed 15685 137 | SBDF Semantic branded drug and form 15456 138 | SCDG Semantic clinical drug group 14793 139 | SCDF Semantic clinical drug and form 13951 140 | IN Name for an ingredient 5017 141 | MIN name for a multi-ingredient 3734 142 | PIN Name from a precise ingredient 1570 143 | BPCK Branded Drug Delivery Device 443 144 | GPCK Generic Drug Delivery Device 391 145 | DF Dose Form 155 146 | DFG Dose Form Group 21 147 | ET Entry term 20 148 | """ 149 | drug_types = ['SCD', 'SCDC', 'SBDG', 'SBD', 'SBDC', 'BN', 'SBDF', 'SCDG', 'SCDF', 'IN', 'MIN', 'PIN', 'BPCK', 'GPCK'] 150 | 151 | cs=conn.execute("select RXCUI, TTY from RXNCONSO where SAB='RXNORM' and TTY in %s"%instr(drug_types)) 152 | 153 | db.concepts.remove({}) 154 | while True: 155 | c = cs.fetchone() 156 | if c == None: break 157 | ii = toIngredients([c[0]], c[1]) 158 | label = conn.execute("select str from rxnconso where SAB='RXNORM' and TTY in %s and rxcui='%s'"%(instr(drug_types), c[0])).fetchone()[0] 159 | r = { 160 | '_id': c[0], 161 | 'label': label, 162 | 'type': c[1], 163 | 'ingredients':list(ii), 164 | 'generics': list(toBrandAndGeneric([c[0]], c[1])), 165 | 'components': list(toComponents([c[0]], c[1])), 166 | # 'mechanisms': list(toMechanism(ii, "IN")), 167 | 'treatmentIntents': list(toTreatmentIntents(ii, "IN")) 168 | } 169 | print json.dumps(r, sort_keys=True, indent=2) 170 | db.concepts.insert(r) 171 | -------------------------------------------------------------------------------- /value_sets/rxnorm/import_sqlite: -------------------------------------------------------------------------------- 1 | rrf$ sed "s/^\(.*\)|$/\1/" RXNREL.RRF > RXNREL.RRF.sl 2 | rrf$ sed "s/^\(.*\)|$/\1/" RXNCONSO.RRF > RXNCONSO.RRF.sl 3 | sqlite3 rxn.db < ../scripts/mysql/Table_scripts_mysql_rxn.sql 4 | sqlite3 rxn.db 5 | sqlite> .import ./RXNCONSO.RRF.sl rxnconso 6 | sqlite> .import ./RXNREL.RRF.sl rxnrel 7 | CREATE INDEX X_RXNCONSO_STR ON RXNCONSO(STR); 8 | CREATE INDEX X_RXNCONSO_RXCUI ON RXNCONSO(RXCUI); 9 | CREATE INDEX X_RXNCONSO_TTY ON RXNCONSO(TTY); 10 | CREATE INDEX X_RXNCONSO_CODE ON RXNCONSO(CODE); 11 | 12 | CREATE INDEX X_RXNREL_RXCUI1 ON RXNREL(RXCUI1); 13 | CREATE INDEX X_RXNREL_RXCUI2 ON RXNREL(RXCUI2); 14 | CREATE INDEX X_RXNREL_RELA ON RXNREL(RELA); 15 | 16 | create index X_RXNREL_SUBJECT_PREDICATE on RXNREL(RXCUI2, RELA); 17 | create index X_RXNREL_ATOM_PREDICATE on RXNREL(RXAUI2, RELA); 18 | ...> ; 19 | --> 714MB rxn.db 20 | --------------------------------------------------------------------------------