├── server
├── __init__.py
├── models.py
├── templates
│ ├── registration
│ │ └── login.html
│ └── base.html
├── urls.py
├── settings.py
└── views.py
├── .dockerignore
├── client
├── templates
│ └── study.tmpl
├── css
│ ├── button-icons.png
│ ├── Aristo
│ │ ├── images
│ │ │ ├── bg_fallback.png
│ │ │ ├── icon_sprite.png
│ │ │ ├── progress_bar.gif
│ │ │ ├── slider_handles.png
│ │ │ ├── ui-icons_222222_256x240.png
│ │ │ └── ui-icons_454545_256x240.png
│ │ └── jquery-ui-1.8.7.custom.css
│ ├── handheld.css
│ ├── nanoscroller.css
│ ├── reportaconcern.css
│ ├── style.css
│ └── button-styles.css
├── js
│ ├── config.js
│ ├── plugins.js
│ ├── libs
│ │ ├── jquery.mousewheel.js
│ │ ├── jquery.nanoscroller.min.js
│ │ ├── big.min.js
│ │ ├── raphaelle.js
│ │ ├── underscore-min.js
│ │ └── require.js
│ ├── utils.js
│ ├── models.js
│ ├── reportaconcern.js
│ ├── lightbox.js
│ ├── views.js
│ ├── main.js
│ └── ruler.js
└── index.html
├── StudyCentric.png
├── wsgi.py
├── env_vars_sample
├── .gitignore
├── Dockerfile
├── scripts
├── http.sh
├── nginx.sh
└── write_config.py
├── LICENSE
└── README.md
/server/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | env_vars*
2 | env_var*
3 |
--------------------------------------------------------------------------------
/client/templates/study.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/StudyCentric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/StudyCentric.png
--------------------------------------------------------------------------------
/client/css/button-icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/button-icons.png
--------------------------------------------------------------------------------
/client/css/Aristo/images/bg_fallback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/bg_fallback.png
--------------------------------------------------------------------------------
/client/css/Aristo/images/icon_sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/icon_sprite.png
--------------------------------------------------------------------------------
/client/css/Aristo/images/progress_bar.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/progress_bar.gif
--------------------------------------------------------------------------------
/client/css/Aristo/images/slider_handles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/slider_handles.png
--------------------------------------------------------------------------------
/client/css/Aristo/images/ui-icons_222222_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/ui-icons_222222_256x240.png
--------------------------------------------------------------------------------
/client/css/Aristo/images/ui-icons_454545_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/ui-icons_454545_256x240.png
--------------------------------------------------------------------------------
/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | os.environ.setdefault('DJANGO_SETTINGS_MODULE',
3 | 'server.settings')
4 |
5 | from django.core.wsgi import get_wsgi_application
6 | application = get_wsgi_application()
7 |
--------------------------------------------------------------------------------
/client/css/handheld.css:
--------------------------------------------------------------------------------
1 | * {
2 | float: none; /* Screens are not big enough to account for floats */
3 | background: #fff; /* As much contrast as possible */
4 | color: #000;
5 | }
6 |
7 | /* Slightly reducing font size to reduce need to scroll */
8 | body { font-size: 80%; }
--------------------------------------------------------------------------------
/env_vars_sample:
--------------------------------------------------------------------------------
1 | SECRET_KEY=django_secret_key
2 | LOGIN_ENABLED=Force users to login, 0 or 1
3 | FORCE_SCRIPT_NAME=
4 | DICOM_SERVER=
5 | DICOM_PORT=
6 |
7 | SERVER_WADO_SERVER=If not supplied, defaults to DICOM_SERVER
8 | SERVER_WADO_PORT=
9 | SERVER_WADO_PATH=
10 | DICOM_AET=DCM4CHEE
--------------------------------------------------------------------------------
/server/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 | {% if form.errors %}
6 |
Your username and password didn't match. Please try again.
7 | {% endif %} 8 | 9 | 25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /client/js/config.js: -------------------------------------------------------------------------------- 1 | // Configuration Options for StudyCentric 2 | define({ 3 | StudyCentricProt:"http", 4 | StudyCentricHost:"localhost", 5 | StudyCentricPath:"", 6 | StudyCentricPort:8000, 7 | WADOHost:"localhost", 8 | WADOPort:8080, 9 | WADOProt:"http", 10 | WADOPath:"wado", 11 | InstanceThumbNailSizePx:100, 12 | SeriesThumbNailSizePx:150, 13 | DefaultImgSize:128, 14 | ImagesPerRow:3, 15 | DisableClinicalWarning:false, 16 | MeasurementPrecision:1, 17 | JSONP: false, 18 | HoverColor: "#FFAA56", 19 | HintColor: "#FFFFFF", 20 | MeasurementColor: "#00FF00", 21 | png:false, 22 | EnableReportConcern:false, 23 | ReportConcernUrl:"../concerns/", 24 | ReportConcernEmail:"Enter Email" 25 | }); 26 | -------------------------------------------------------------------------------- /client/js/plugins.js: -------------------------------------------------------------------------------- 1 | 2 | // usage: log('inside coolFunc', this, arguments); 3 | // paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ 4 | window.log = function(){ 5 | log.history = log.history || []; // store logs to an array for reference 6 | log.history.push(arguments); 7 | arguments.callee = arguments.callee.caller; 8 | if(this.console) console.log( Array.prototype.slice.call(arguments) ); 9 | }; 10 | // make it safe to use console.log always 11 | (function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();)b[a]=b[a]||c})(window.console=window.console||{}); 12 | 13 | 14 | // place any jQuery/helper plugins in here, instead of separate, slower script files. 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.py[co] 4 | *.sw[po] 5 | dist 6 | sdist 7 | build 8 | pip-log.txt 9 | MANIFEST 10 | .Python 11 | .watch-pid 12 | .sass-cache 13 | testdb 14 | lib/ 15 | bin/ 16 | include/ 17 | gdcm/ 18 | gdcmbin/ 19 | 20 | # ignore local settings file intended for environment-specific settings 21 | local_settings.py 22 | 23 | # ignore optimized javascripts 24 | src/static/scripts/javascript/min 25 | 26 | # all Sass files will be compiled into this directory 27 | src/static/stylesheets/css/*.css 28 | src/static/stylesheets/css/page/*.css 29 | 30 | # Collected static files via Django's staticfiles app 31 | _site/static 32 | 33 | # Do not version user uploaded content since these files might be huge. 34 | # Have another strategy for backing up this content. 35 | _site/media 36 | env_vars_chop 37 | env_var_chop 38 | build.sh 39 | env_vars* 40 | -------------------------------------------------------------------------------- /server/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |Horizontal Spacing: <%=xSpacing%>
Vertical Spacing: <%=ySpacing%>
"); 5 | return { 6 | queryString2Object: function(queryString){ 7 | var regEx = /[?&]([^=]*)=([^&]*)/g; 8 | var match; 9 | var obj = {}; 10 | while(match = regEx.exec(queryString)){ 11 | obj[match[1]]=match[2]; 12 | } 13 | return obj; 14 | }, 15 | processDicomAttributes: function(attrs){ 16 | // if the image displayed does not match this response 17 | // we are done 18 | var lightBox = $("#lightbox"); 19 | if (attrs.objectUID !== this.queryString2Object(lightBox[0].src).objectUID) 20 | return; 21 | // TODO disable zoom if nativeRows is 0 22 | lightBox.data("dicom_attrs", attrs); 23 | if (attrs.xSpacing) { 24 | attrs.xSpacing = parseFloat(attrs.xSpacing); 25 | attrs.ySpacing = parseFloat(attrs.ySpacing); 26 | $("#pixel_spacing").html(t_withPixel(attrs)); 27 | } 28 | else $("#pixel_spacing").text("Measurements in pixels"); 29 | // If we don't have windowWidth params, default to sensible 30 | if (!attrs.windowWidth || !attrs.windowCenter) attrs.defaults = true; 31 | attrs.windowWidth = attrs.windowWidth || 50; 32 | attrs.windowCenter = attrs.windowCenter || 200; 33 | //TODO set message for native resolution here, and specify whether it existed or not. 34 | attrs.nativeCols = attrs.nativeCols || 512; 35 | attrs.nativeRows = attrs.nativeRows || 512; 36 | $("body").trigger("dicom_attr_received"); 37 | if (attrs.pixelMessage) $("#pixel_message").text(attrs.pixelMessage); 38 | }, 39 | retrieveDicomAttributes: function(study,imgSource){ 40 | if (timeout !== null){ 41 | clearTimeout(timeout); 42 | } 43 | $("body").trigger("null_dicom_attr"); 44 | if (req !== null) req.abort(); 45 | var that = this; 46 | timeout = setTimeout(function() { 47 | var o = that.queryString2Object(imgSource); 48 | var url = (Config.StudyCentricHost ? ((Config.StudyCentricProt || "https") + "://" + Config.StudyCentricHost + (Config.StudyCentricPort ? ":" + Config.StudyCentricPort:"") +"/") : "" ) + Config.StudyCentricPath +"object/"+o.objectUID+"?&seriesUID="+o.seriesUID+"&studyUID="+o.studyUID; 49 | if (Config.JSONP) 50 | url += "&callback=?"; 51 | req = $.getJSON(url, $.proxy(that.processDicomAttributes, that) ); 52 | }, 500); 53 | } 54 | }; 55 | }); 56 | -------------------------------------------------------------------------------- /client/js/models.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 2 | 'config', 3 | 'backbone', 4 | 'views'], function($, Config, Backbone, Views) { 5 | 6 | var Instance = Backbone.Model.extend({ 7 | initialize:function(){ 8 | var view = new Views.InstanceView({model:this}); 9 | // TODO this awful, its here to enable putting the red box around instances during scroll 10 | this.bind("scroll_show", function(){$("img", view.el).trigger("click");}); 11 | } 12 | }); 13 | 14 | var ImageStack = Backbone.Collection.extend({ 15 | model:Instance, 16 | parse: function(response){ 17 | var models = []; 18 | for (var index in response){ 19 | models.push({uid:response[index], studyUID:this.studyUID, seriesUID:this.seriesUID}); 20 | } 21 | return models; 22 | }, 23 | initialize: function(){ 24 | //var view = new Views.InstanceStackView({collection:this}); 25 | } 26 | }); 27 | 28 | var Series = Backbone.Model.extend({ 29 | initialize: function(){ 30 | var ref = this; 31 | var data = "json"; 32 | this.instances = new ImageStack(); 33 | // Store study and instance uid on collection so we can properly populate instance model 34 | this.instances.studyUID = this.get("studyUID"); 35 | this.instances.seriesUID=this.get("uid"); 36 | this.instances.url = (Config.StudyCentricHost ? ((Config.StudyCentricProt || "https") + "://" + Config.StudyCentricHost + (Config.StudyCentricPort ? ":"+Config.StudyCentricPort : "") + "/") : "" ) + Config.StudyCentricPath + "series/"+this.get("uid")+"?callback=?"; 37 | var view = new Views.SeriesView({model:this}); 38 | if (Config.JSONP) 39 | data = "jsonp"; 40 | this.instances.fetch({dataType:data}); 41 | } 42 | }); 43 | 44 | var SeriesStack = Backbone.Collection.extend({ 45 | model:Series, 46 | parse: function(response){ 47 | var models = []; 48 | var series_objs=response["series"]; 49 | for (var index in series_objs){ 50 | models.push({uid:series_objs[index]["uid"], description:series_objs[index]["description"], studyUID:this.studyUID}); 51 | } 52 | // TODO remove this 53 | $("#study_description").text(response.description); 54 | return models; 55 | } 56 | }); 57 | 58 | var Study = Backbone.Model.extend({ 59 | initialize: function(){ 60 | var data = "json"; 61 | this.series = new SeriesStack(); 62 | // Stores study uid on collection so we can populate series model 63 | this.series.studyUID = this.get("uid"); 64 | this.series.url = (Config.StudyCentricHost ? ((Config.StudyCentricProt || "https")+"://"+Config.StudyCentricHost + (Config.StudyCentricPort ? ":"+Config.StudyCentricPort : "") + "/") : "") + Config.StudyCentricPath + "study/" + this.get("uid")+"?callback=?"; 65 | if (Config.JSONP) 66 | data = "jsonp"; 67 | this.series.fetch({dataType:data}); 68 | } 69 | }); 70 | 71 | return { 72 | ImageStack:ImageStack, 73 | SeriesStack:SeriesStack, 74 | Series:Series, 75 | Study:Study, 76 | Instance:Instance 77 | }; 78 | }); 79 | -------------------------------------------------------------------------------- /client/css/reportaconcern.css: -------------------------------------------------------------------------------- 1 | .ui-dialog { 2 | -moz-box-shadow: 0 5px 20px 0 #666666; 3 | -webkit-box-shadow: 0 5px 20px 0 #666666; 4 | box-shadow: 0 5px 20px 0 #666666; 5 | position: absolute; } 6 | 7 | .ui-dialog > .container { 8 | -moz-box-shadow: none; 9 | -webkit-box-shadow: none; 10 | box-shadow: none; } 11 | 12 | .ui-dialog.ui-corner-all { 13 | -moz-border-radius: 3px 3px 0 0; 14 | border-radius: 3px 3px 0 0; } 15 | 16 | .ui-dialog.ui-widget-content, .ui-dialog > .ui-widget-content { 17 | border: none; 18 | background-image: none; 19 | color: inherit; } 20 | 21 | .ui-dialog.ui-widget { 22 | font: inherit; } 23 | 24 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header { 25 | -moz-border-radius: 4px 4px 0 0; 26 | border-radius: 4px 4px 0 0; 27 | color: #eeeeee; 28 | text-shadow: 0 1px 2px #3b3b3b; 29 | background-color: #444; 30 | -moz-box-shadow: inset 0 1px 2px #919191, inset 0 -1px 2px #2b2b2b; 31 | -webkit-box-shadow: inset 0 1px 2px #919191, inset 0 -1px 2px #2b2b2b; 32 | box-shadow: inset 0 1px 2px #919191, inset 0 -1px 2px #2b2b2b; 33 | border: 1px solid #111111; 34 | padding: 4px 10px; 35 | font-weight: 500; 36 | font-size: 1.1em; 37 | background-image: none; } 38 | 39 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header::after { 40 | border-bottom: 1px solid #fff; } 41 | 42 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header .ui-dialog-title { 43 | text-shadow: 0 1px 2px #3B3B3B; 44 | margin: 0; } 45 | 46 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header .ui-dialog-titlebar-close { 47 | display: none; } 48 | 49 | .ui-dialog .ui-dialog-content, .ui-dialog .ui-dialog-content > .content { 50 | background: #eee; } 51 | 52 | .ui-dialog .container.ui-dialog-content { 53 | padding-top: 0; 54 | } 55 | 56 | .ui-dialog .ui-dialog-buttonpane { 57 | font-size: 0.8em; 58 | margin: 0 !important; 59 | padding: 0 10px 10px 10px !important; 60 | text-align: right; 61 | background-color: #eee; 62 | border: none !important; } 63 | 64 | .ui-dialog .ui-dialog-buttonpane button, .ui-dialog .ui-dialog-buttonpane button { 65 | margin-left: 10px; } 66 | 67 | 68 | #rac { 69 | -moz-border-radius: 3px; 70 | border-radius: 3px; 71 | background-color: #dddddd; 72 | border: 1px solid #aaaaaa; 73 | -moz-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #c4c4c4, 0 1px 2px #aaaaaa; 74 | -webkit-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #c4c4c4, 0 1px 2px #aaaaaa; 75 | box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #c4c4c4, 0 1px 2px #aaaaaa; 76 | color: #222222; 77 | text-shadow: 0 1px 2px #d4d4d4; 78 | cursor: pointer; 79 | padding: 2px 5px; 80 | font-weight: 600; 81 | font-size: 0.9em; 82 | border: 1px solid #666; 83 | background-color: #ff9999; 84 | border: 1px solid red; 85 | -moz-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #ff6666, 0 1px 2px #d65c5c; 86 | -webkit-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #ff6666, 0 1px 2px #d65c5c; 87 | box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #ff6666, 0 1px 2px #d65c5c; 88 | padding: 1px 5px; 89 | color: #330000; 90 | text-decoration:none; 91 | } 92 | 93 | #rac.success { 94 | background-color: #00cc00; 95 | border: 1px solid #003300; 96 | -moz-box-shadow: inset 0 1px 2px #85e085, inset 0 -1px 2px #009900, 0 1px 2px #145214; 97 | -webkit-box-shadow: inset 0 1px 2px #85e085, inset 0 -1px 2px #009900, 0 1px 2px #145214; 98 | box-shadow: inset 0 1px 2px #85e085, inset 0 -1px 2px #009900, 0 1px 2px #145214; 99 | color: black; } 100 | 101 | #rac-dialog { 102 | display: none; } 103 | 104 | #rac-dialog textarea { 105 | height: 100px; 106 | width: 368px; 107 | font-size: 0.85em; 108 | } 109 | 110 | #rac:active{ 111 | top:17px; 112 | } 113 | -------------------------------------------------------------------------------- /client/js/libs/jquery.nanoscroller.min.js: -------------------------------------------------------------------------------- 1 | define(["jquery"],function(jQuery){(function(){var c,e,d=function(b,a){return function(){return b.apply(a,arguments)}};c=this.jQuery;e=function(){function b(a){this.el=a;this.generate();this.createEvents();this.addEvents();this.reset()}b.prototype.createEvents=function(){return this.events={down:d(function(a){this.isDrag=!0;this.offsetY=a.clientY-this.slider.offset().top;this.pane.addClass("active");c(document).bind("mousemove",this.events.drag);c(document).bind("mouseup",this.events.up);return!1},this),drag:d(function(a){this.sliderY= 2 | a.clientY-this.el.offset().top-this.offsetY;this.scroll();return!1},this),up:d(function(){this.isDrag=!1;this.pane.removeClass("active");c(document).unbind("mousemove",this.events.drag);c(document).unbind("mouseup",this.events.up);return!1},this),resize:d(function(){return this.reset()},this),panedown:d(function(a){this.sliderY=a.clientY-this.el.offset().top-0.5*this.sliderH;this.scroll();return this.events.down(a)},this),scroll:d(function(){if(!0!==this.isDrag)return this.slider.css({top:this.content[0].scrollTop/ 3 | (this.content[0].scrollHeight-this.content[0].clientHeight)*(this.paneH-this.sliderH)+"px"})},this),wheel:d(function(a){this.sliderY+=-a.wheelDeltaY||-a.delta;this.scroll();return!1},this)}};b.prototype.addEvents=function(){c(window).bind("resize",this.events.resize);this.slider.bind("mousedown",this.events.down);this.pane.bind("mousedown",this.events.panedown);this.content.bind("scroll",this.events.scroll);if(window.addEventListener)return this.pane[0].addEventListener("mousewheel",this.events.wheel), 4 | this.pane[0].addEventListener("DOMMouseScroll",this.events.wheel)};b.prototype.removeEvents=function(){c(window).unbind("resize",this.events.resize);this.slider.unbind("mousedown",this.events.down);this.pane.unbind("mousedown",this.events.panedown);this.content.unbind("scroll",this.events.scroll);if(window.addEventListener)return this.pane[0].removeEventListener("mousewheel",this.events.wheel),this.pane[0].removeEventListener("DOMMouseScroll",this.events.wheel)};b.prototype.getScrollbarWidth=function(){var a, 5 | b,c;b=document.createElement("div");b.style.position="absolute";b.style.width="100px";b.style.height="100px";b.style.overflow="scroll";document.body.appendChild(b);a=b.offsetWidth;c=b.scrollWidth;document.body.removeChild(b);return a-c};b.prototype.generate=function(){this.el.append('