├── .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 |
7 | My Dropdown Menu
8 |
13 |
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 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
Help improve the Scorecard!
40 |
Four ways to help improve the SMART C-CDA Scorecard:
41 |
42 | Try out the Scorecard and tweet about it @SMARTHealthIT
43 | Share your sample C-CDA documents with the public
44 | Suggest new rubrics , or improvements to the existing ones
45 | Contribute code to improve the Scorecard
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 |
83 |
84 |
85 |
86 |
90 |
91 |
It looks like your browser doesn't support the SMART C-CDA Scorecard. You can try anyway, but we recommend
92 |
98 |
99 |
102 |
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 | Give permission
30 | No thanks
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/public/ccdaScorecard/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Your C-CDA. Beautiful.
6 |
7 |
8 |
9 |
43 |
44 |
45 | {{errors[0]}}
46 |
47 |
48 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | {{rubric.description}}
90 |
91 |
92 |
93 |
94 | N/A
95 |
96 |
97 | {{score.score}}/{{rubric.maxPoints}} points
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
112 |
113 |
114 |
Best Practice: {{rubric.detail}}
115 |
116 |
117 |
118 | Your Results:
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | No matches -- try broadening your search.
34 |
35 |
36 |
37 |
38 |
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 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
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 |
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 | Your code
9 | What now?
10 |
11 |
12 | <% misses.forEach(function(miss){ %>
13 |
14 |
15 | <%= miss.codeSystemName %>:<%= miss.code %>
16 |
17 |
18 | "<%= miss.displayName %>"
19 |
20 | Check mapping
21 |
22 | <% }) %>
23 |
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 | Your code
9 | Preferred term
10 | What now?
11 |
12 |
13 | <% misses.forEach(function(miss){ %>
14 |
15 |
16 | <%= miss.codeSystemName %>:<%= miss.code %>
17 |
18 |
19 | "<%= miss.displayName %>"
20 |
21 |
22 | "<%= miss.normalized.conceptName %>"
23 |
24 |
25 | See <%= miss.normalized._id %>
26 |
27 |
28 | <% }) %>
29 |
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 | Your code
4 | The issue
5 | What now?
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 |
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 |
26 | <% if (miss.displayName) { %>
27 | "<%= miss.displayName %>"
28 | <% } else { %>
29 | (No title)
30 | <% } %>
31 | <%= issue %>
32 | <% if (miss.normalized && !miss.nullFlavor){%>
33 | See <%= miss.normalized._id %>
34 | <%} else {%>
35 | Check mapping
36 | <%}%>
37 |
38 |
39 | <%
40 | });
41 | %>
42 |
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 |
22 | <%= m %>
23 |
24 | <%})%>
25 |
26 | <% }) %>
27 |
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 | Old templateId
11 | Times used
12 | Official C-CDA templateId
13 |
14 |
15 | <% Object.keys(misses).forEach(function(miss){ %>
16 |
17 |
18 | <%= miss %>
19 |
20 | <%= misses[miss].length%>
21 |
22 |
23 | <% misses[miss][0].goodIds.forEach(function(id){ %>
24 |
25 | <%= id %>
26 |
27 | <% }) %>
28 |
29 |
30 |
31 | <% }) %>
32 |
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 |
18 |
19 | HITSP Vital Sign Result
20 |
21 |
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 | Vital
9 | Value
10 | Your units
11 | Preferred units
12 | What now?
13 |
14 |
15 | <% misses.forEach(function(miss){ %>
16 |
17 |
18 |
19 | <%= miss.code.normalized.codeSystemName %>:<%= miss.code.normalized.code %>
20 |
21 |
22 |
23 | <%= miss.code.normalized.conceptName %>
24 |
25 | "<%= miss.value %>"
26 | <%= miss.unit %>
27 |
28 |
29 | <% miss.preferred.forEach(function(preferred){ %>
30 | <%= preferred %>
31 | <% }); %>
32 |
33 |
34 |
35 | Check mapping to UCUM
36 |
37 |
38 | <% }) %>
39 |
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 |
--------------------------------------------------------------------------------