{{ #indent }}
-
-
- {{ name }}
-
+
{{ /indent }}
{{ ^indent }}
@@ -36,11 +39,13 @@
{/literal}
diff --git a/modules/examiner/README.md b/modules/examiner/README.md
new file mode 100644
index 00000000000..2d51836aec2
--- /dev/null
+++ b/modules/examiner/README.md
@@ -0,0 +1,92 @@
+# Examiner
+
+## Purpose
+
+The Examiner module displays the list of examiners in the study and allows
+for additions and removal of certifications for each examiner listed.
+
+## Intended Users
+
+The primary type of users is:
+1. Site coordinators adding examiners and adding certifications to examiners
+
+
+## Scope
+
+This module displays information about examiners as well as certifications and
+sites to which the examiners are affiliated. It also allows adding examiners
+to sites, to give or remove certifications for instruments and set them as
+radiologists.
+
+NOT in scope:
+
+Management of the certification instruments and of the sites is external to
+this module.
+
+The Examiner architecture is completely separate from the User
+architecture intentionally; examiners may or may not be users.
+
+
+## Permissions
+
+The Examiner module uses the following permissions. Any one of them
+is sufficient to have access to the module.
+
+examiner_view
+ - This permission gives the user viewing and editing access to all
+ examiners and certifications within their own site
+
+examiner_multisite
+ - This permission gives the user viewing and editing access to all
+ examiners and certifications at all sites.
+
+## Configurations
+
+The examiner module has the following configurations that affect its usage
+
+EnableCertification
+ - Binary entry for sub-tag `
` under the tag
+ `` in the config.xml file. Options `1` or `0` enable or
+ disable, respectively, the use of certification for projects defined
+ in the `` section or all projects if none are
+ explicitly stated.
+
+
+CertificationProjects (Config.xml)
+ - Projects for which certification is enabled, should match entries in
+ `Project` table in the database. The project identifier should be placed
+ within the `` tags as such
+ `1 `.
+
+CertificationInstruments
+ - Instruments which require certification to be able to administer.
+ When an instrument is added to this list, the examiner dropdown found
+ at the top page this instrument is populated only with examiners having
+ obtained a certification for it.
+
+startYear
+ - Min date of certification. This is entry is also the start year of the study.
+
+endYear
+ - Max date of certification. This is entry is also the end year of the study.
+
+## Interactions with LORIS
+
+- The list of examiners displayed at the top page of instruments is
+dependent on entries in this module. The list is populated
+with examiners that exist in the database, are at the same site
+and have the appropriate certifications (if configured); failure to
+match these 3 criteria will result in the examiner not being displayed.
+
+- The user_accounts module allows for quick activation/deactivation of
+an examiner as well as setting their sites and their radiologist status.
+When an examiner is added from the user_accounts module, the `userID` field of
+the `examiners` table in the database is populated to associate the examiner
+to the user.
+
+- The Training module allows the study to automatically assign
+certification upon successful completion of training. The module
+starts by verifying that the user is indeed an examiner
+before beginning the training. The certifications should appear
+in the examiner module once obtained by an examiner and it should
+be dated with the date of completion of the training process.
diff --git a/modules/examiner/php/editexaminer.class.inc b/modules/examiner/php/editexaminer.class.inc
index 20a74d65a91..8906b0e64f1 100644
--- a/modules/examiner/php/editexaminer.class.inc
+++ b/modules/examiner/php/editexaminer.class.inc
@@ -142,16 +142,20 @@ class EditExaminer extends \NDB_Form
// if certification for new instrument for the examiner
if (empty($certID) && !empty($certStatus)) {
// insert a new certification entry
+ $data = array(
+ 'examinerID' => $examinerID,
+ 'testID' => $testID,
+ 'pass' => $certStatus,
+ 'comment' => $comment,
+ );
+ if ($date_cert != "") {
+ $data['date_cert'] = $date_cert;
+ }
$DB->insert(
'certification',
- array(
- 'examinerID' => $examinerID,
- 'date_cert' => $date_cert,
- 'testID' => $testID,
- 'pass' => $certStatus,
- 'comment' => $comment,
- )
+ $data
);
+
$certID = $DB->pselectOne(
"SELECT certID
FROM certification
@@ -162,19 +166,23 @@ class EditExaminer extends \NDB_Form
)
);
// Add a new entry to the certification history table
+ $data = array(
+ 'col' => 'pass',
+ 'new' => $certStatus,
+ 'primaryVals' => $certID,
+ 'testID' => $testID,
+ 'visit_label' => $visit_label,
+ 'changeDate' => date("Y-m-d H:i:s"),
+ 'userID' => $_SESSION['State']->getUsername(),
+ 'type' => 'I',
+ );
+ if ($date_cert != "") {
+ $data['new_date'] = $date_cert;
+ }
+
$DB->insert(
'certification_history',
- array(
- 'col' => 'pass',
- 'new' => $certStatus,
- 'new_date' => $date_cert,
- 'primaryVals' => $certID,
- 'testID' => $testID,
- 'visit_label' => $visit_label,
- 'changeDate' => date("Y-m-d H:i:s"),
- 'userID' => $_SESSION['State']->getUsername(),
- 'type' => 'I',
- )
+ $data
);
} else { // update to a test certification for the examiner
@@ -210,13 +218,16 @@ class EditExaminer extends \NDB_Form
|| $oldCertification['date_cert'] != $date_cert
) {
// Update the certification entry
+ $data = array(
+ 'pass' => $certStatus,
+ 'comment' => $comment,
+ );
+ if ($date_cert != "") {
+ $data['date_cert'] = $date_cert;
+ }
$DB->update(
'certification',
- array(
- 'date_cert' => $date_cert,
- 'pass' => $certStatus,
- 'comment' => $comment,
- ),
+ $data,
array(
'examinerID' => $examinerID,
'testID' => $testID,
@@ -225,21 +236,26 @@ class EditExaminer extends \NDB_Form
// Add a new entry to the certification history table
if ($oldDate != $date_cert || $oldVal != $certStatus) {
+ $data = array(
+ 'col' => 'pass',
+ 'old' => $oldVal,
+ 'new' => $certStatus,
+ 'primaryVals' => $certID,
+ 'testID' => $testID,
+ 'visit_label' => $visit_label,
+ 'changeDate' => date("Y-m-d H:i:s"),
+ 'userID' => $_SESSION['State']->getUsername(),
+ 'type' => 'U',
+ );
+ if ($oldDate != "") {
+ $data['old_date'] = $oldDate;
+ }
+ if ($date_cert != "") {
+ $data['new_date'] = $date_cert;
+ }
$DB->insert(
'certification_history',
- array(
- 'col' => 'pass',
- 'old' => $oldVal,
- 'old_date' => $oldDate,
- 'new' => $certStatus,
- 'new_date' => $date_cert,
- 'primaryVals' => $certID,
- 'testID' => $testID,
- 'visit_label' => $visit_label,
- 'changeDate' => date("Y-m-d H:i:s"),
- 'userID' => $_SESSION['State']->getUsername(),
- 'type' => 'U',
- )
+ $data
);
}
}
@@ -353,6 +369,7 @@ class EditExaminer extends \NDB_Form
*/
function _validateEditExaminer($values)
{
+ $DB = \Database::singleton();
$errors = array();
// check that there is both a status and a date (neither can be null)
@@ -360,8 +377,8 @@ class EditExaminer extends \NDB_Form
if (empty($certificationStatus)
|| empty($values['date_cert'][$instrumentID])
) {
- if (!(empty($certificationStatus)
- && empty($values['date_cert'][$instrumentID]))
+ if (($certificationStatus == "certified")
+ && empty($values['date_cert'][$instrumentID])
) {
$errors['certStatus[' . $instrumentID .']'] = 'Both certification
status and date must be filled out';
@@ -369,6 +386,20 @@ class EditExaminer extends \NDB_Form
}
}
+ // check if previously recorded certification are all present
+ // (can not delete, only change status)
+ $rows = $DB->pselect(
+ "SELECT c.testID
+ FROM certification c
+ WHERE c.examinerID=:EID",
+ array('EID' => $this->identifier)
+ );
+ foreach ($rows as $row) {
+ if ($values['certStatus'][$row['testID']] == "") {
+ $errors['certStatus['.$row['testID'].']']
+ = 'You can not delete a status';
+ }
+ }
return $errors;
}
diff --git a/modules/examiner/php/examiner.class.inc b/modules/examiner/php/examiner.class.inc
index 8150493532a..303f94f873a 100644
--- a/modules/examiner/php/examiner.class.inc
+++ b/modules/examiner/php/examiner.class.inc
@@ -42,7 +42,7 @@ class Examiner extends \NDB_Menu_Filter_Form
/**
* Sets the menu filter class variables.
*
- * @return boolean
+ * @return void
*/
function _setupVariables()
{
@@ -130,7 +130,7 @@ class Examiner extends \NDB_Menu_Filter_Form
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return null
+ * @return void
*/
function setup()
{
@@ -206,7 +206,7 @@ class Examiner extends \NDB_Menu_Filter_Form
*
* @param array $values add examiner form values
*
- * @return void
+ * @return bool
*/
function _process($values)
{
@@ -267,6 +267,7 @@ class Examiner extends \NDB_Menu_Filter_Form
);
header("Location: {$baseurl}/examiner/", true, 303);
+ return true;
}
/**
diff --git a/modules/genomic_browser/php/cnv_browser.class.inc b/modules/genomic_browser/php/cnv_browser.class.inc
index 39da469d98b..41d06248476 100644
--- a/modules/genomic_browser/php/cnv_browser.class.inc
+++ b/modules/genomic_browser/php/cnv_browser.class.inc
@@ -58,8 +58,7 @@ class CNV_Browser extends \NDB_Menu_Filter
/**
* Function _setupVariables
*
- * @note overloaded function
- * @return bool
+ * @return void
*/
function _setupVariables()
{
@@ -209,7 +208,7 @@ class CNV_Browser extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/genomic_browser/php/cpg_browser.class.inc b/modules/genomic_browser/php/cpg_browser.class.inc
index 2817dddd6ed..f7b4559ec12 100644
--- a/modules/genomic_browser/php/cpg_browser.class.inc
+++ b/modules/genomic_browser/php/cpg_browser.class.inc
@@ -60,8 +60,7 @@ class CpG_Browser extends \NDB_Menu_Filter
/**
* Function _setupVariables
*
- * @note overloaded function
- * @return bool
+ * @return void
*/
function _setupVariables()
{
@@ -253,7 +252,7 @@ class CpG_Browser extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/genomic_browser/php/genomic_browser.class.inc b/modules/genomic_browser/php/genomic_browser.class.inc
index 62930c7b70b..f148ebc933f 100644
--- a/modules/genomic_browser/php/genomic_browser.class.inc
+++ b/modules/genomic_browser/php/genomic_browser.class.inc
@@ -60,7 +60,7 @@ class Genomic_Browser extends \NDB_Menu_Filter
* Function _setupVariables
*
* @note overloaded function
- * @return bool
+ * @return void
*/
function _setupVariables()
{
@@ -190,7 +190,7 @@ class Genomic_Browser extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/genomic_browser/php/genomic_file_uploader.class.inc b/modules/genomic_browser/php/genomic_file_uploader.class.inc
index 28421152f42..14002635eeb 100644
--- a/modules/genomic_browser/php/genomic_file_uploader.class.inc
+++ b/modules/genomic_browser/php/genomic_file_uploader.class.inc
@@ -48,8 +48,7 @@ class Genomic_File_Uploader extends \NDB_Menu_Filter
/**
* Function _setupVariables
*
- * @note overloaded function
- * @return bool
+ * @return void
*/
function _setupVariables()
{
@@ -111,7 +110,7 @@ class Genomic_File_Uploader extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/genomic_browser/php/gwas_browser.class.inc b/modules/genomic_browser/php/gwas_browser.class.inc
index 1eab4ac0798..e32d896e5fe 100644
--- a/modules/genomic_browser/php/gwas_browser.class.inc
+++ b/modules/genomic_browser/php/gwas_browser.class.inc
@@ -52,8 +52,7 @@ class GWAS_Browser extends \NDB_Menu_Filter
/**
* Function _setupVariables
*
- * @note overloaded function
- * @return bool
+ * @return void
*/
function _setupVariables()
{
@@ -118,7 +117,7 @@ class GWAS_Browser extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/genomic_browser/php/snp_browser.class.inc b/modules/genomic_browser/php/snp_browser.class.inc
index 783cccf8a9b..508887ca82e 100644
--- a/modules/genomic_browser/php/snp_browser.class.inc
+++ b/modules/genomic_browser/php/snp_browser.class.inc
@@ -59,8 +59,7 @@ class SNP_Browser extends \NDB_Menu_Filter
/**
* Function _setupVariables
*
- * @note overloaded function
- * @return bool
+ * @return void
*/
function _setupVariables()
{
@@ -233,7 +232,7 @@ class SNP_Browser extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/help_editor/README.md b/modules/help_editor/README.md
new file mode 100644
index 00000000000..862a35be31d
--- /dev/null
+++ b/modules/help_editor/README.md
@@ -0,0 +1,33 @@
+# Help Editor
+
+## Purpose
+
+The help editor module provides a menu to view and edit online help
+for LORIS.
+
+## Intended Users
+
+The help editor is intended to be used by data coordinating staff
+to write the help content for instruments that are part of their
+LORIS instance, but not part of the core LORIS install.
+
+## Scope
+
+Only instrument help text is intended to be edited. Module help
+text for modules that are part of LORIS should come from the markdown
+files which are in the module's `help/` directory.
+
+## Permissions
+
+The `context_help` permission is required to access the help editor
+module.
+
+## Configurations
+
+None.
+
+## Interactions with LORIS
+
+The ajax script used by all LORIS page's inline help to load content
+is part of the Help Editor module. This should likely be revisited in
+the future.
diff --git a/modules/help_editor/js/help_editor_helper.js b/modules/help_editor/js/help_editor_helper.js
index 1e80f09b8aa..d24be44e276 100644
--- a/modules/help_editor/js/help_editor_helper.js
+++ b/modules/help_editor/js/help_editor_helper.js
@@ -9,21 +9,23 @@ $("input[name=preview]").click(function(e) {
content = $('textarea[name="content"]').val(),
myDate = new Date(),
div = document.createElement("div"),
- pre = document.createElement("pre"),
btn = document.createElement("BUTTON"),
- text = document.createTextNode("Edit"),
- button = document.createTextNode("Close");
+ button = document.createTextNode("Close"),
+ wrap = document.createElement("div");
- pre.innerHTML = "" + title + " ";
- pre.innerHTML += content;
- pre.innerHTML = pre.innerHTML + " Last updated: " + myDate.getFullYear() + "-" +
+ wrap.setAttribute("id", "help-wrapper");
+ wrap.innerHTML = "" + title + " ";
+ markdownContent = document.createElement("div");
+ ReactDOM.render(RMarkdown({content: content}), markdownContent);
+ wrap.appendChild(markdownContent);
+ wrap.innerHTML = wrap.innerHTML + " Last updated: " + myDate.getFullYear() + "-" +
(myDate.getMonth() +1) + "-" + myDate.getDate() + " " +
myDate.getHours() + ":" + myDate.getMinutes() + ":" + myDate.getSeconds();
btn.appendChild(button);
btn.className="btn btn-default";
btn.setAttribute("id","helpclose");
- div.appendChild(pre);
div.appendChild(btn);
+ div.appendChild(wrap);
document.getElementById('page').appendChild(div);
div.setAttribute("class", "help-content");
btn.addEventListener("click", function(e) {
diff --git a/modules/help_editor/php/edit_help_content.class.inc b/modules/help_editor/php/edit_help_content.class.inc
index 1ceeb56f4c7..ac871e7aeef 100644
--- a/modules/help_editor/php/edit_help_content.class.inc
+++ b/modules/help_editor/php/edit_help_content.class.inc
@@ -42,7 +42,7 @@ class Edit_Help_Content extends \NDB_Form
/**
* Returns default help content of the page
*
- * @return default array
+ * @return array
*/
function _getDefaults()
{
@@ -50,14 +50,13 @@ class Edit_Help_Content extends \NDB_Form
//Get the default values
// Sanitize user input
- $safeHelpID = htmlspecialchars($_REQUEST['helpID']);
$safeSection = $_REQUEST['section'] ?
htmlspecialchars($_REQUEST['section']) : '';
$safeSubsection = $_REQUEST['subsection'] ?
htmlspecialchars($_REQUEST['subsection']) : '';
if (isset($_REQUEST['helpID'])) {
- $helpID = $safeHelpID;
+ $helpID = htmlspecialchars($_REQUEST['helpID']);
}
if (!empty($_REQUEST['section'])) {
$helpID = HelpFile::hashToID(md5($_REQUEST['section']));
@@ -82,6 +81,8 @@ class Edit_Help_Content extends \NDB_Form
}
}
+ // let escaped html display as intended in form
+ $defaults['content'] = htmlspecialchars_decode($defaults['content']);
// case where no help content exists
if (empty($defaults['title'])) {
if (!empty($_REQUEST['section'])) {
@@ -105,7 +106,7 @@ class Edit_Help_Content extends \NDB_Form
/**
* Process function
*
- * @param string $values the value
+ * @param array $values the values being processed
*
* @return void
*/
@@ -115,15 +116,13 @@ class Edit_Help_Content extends \NDB_Form
//Get the default values
// Sanitize user input
- $safeHelpID = htmlspecialchars($_REQUEST['helpID']);
- $safeParentID = htmlspecialchars($_REQUEST['parentID']);
$safeSection = htmlspecialchars($_REQUEST['section']);
$safeSubsection = htmlspecialchars($_REQUEST['subsection']);
if (isset($_REQUEST['helpID'])) {
- $helpID = $safeHelpID;
+ $helpID = htmlspecialchars($_REQUEST['helpID']);
}
if (isset($_REQUEST['parentID'])) {
- $parentID = $safeParentID;
+ $parentID = htmlspecialchars($_REQUEST['parentID']);
}
if (!empty($_REQUEST['section'])) {
diff --git a/modules/help_editor/php/help_editor.class.inc b/modules/help_editor/php/help_editor.class.inc
index cb928c4d119..406f90ce184 100644
--- a/modules/help_editor/php/help_editor.class.inc
+++ b/modules/help_editor/php/help_editor.class.inc
@@ -40,7 +40,7 @@ class Help_Editor extends \NDB_Menu_Filter
/**
* Setup Variables function
*
- * @return bool
+ * @return void
*/
function _setupVariables()
{
@@ -89,7 +89,7 @@ class Help_Editor extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
@@ -98,15 +98,14 @@ class Help_Editor extends \NDB_Menu_Filter
// add form elements
$this->addBasicText('topic', 'Help topic:');
$this->addBasicText('keyword', 'Search keyword');
-
- return true;
}
+
/**
* SetDataTableRows
*
- * @param string $count the value of count
+ * @param int $count the value of count
*
- * @return bool
+ * @return void
*/
function _setDataTableRows($count)
{
diff --git a/modules/help_editor/php/helpfile.class.inc b/modules/help_editor/php/helpfile.class.inc
index b14cf764482..641cd08ad8e 100644
--- a/modules/help_editor/php/helpfile.class.inc
+++ b/modules/help_editor/php/helpfile.class.inc
@@ -87,7 +87,7 @@ class HelpFile
$DB =& \Database::singleton();
// insert a help file
- $success = $DB->insert('help', $set);
+ $success = $DB->unsafeinsert('help', $set);
// return the help ID
return $DB->lastInsertID;
}
@@ -106,7 +106,7 @@ class HelpFile
$DB =& \Database::singleton();
// update the help file
- $success = $DB->update('help', $set, array('helpID' => $this->helpID));
+ $success = $DB->unsafeupdate('help', $set, array('helpID' => $this->helpID));
return true;
}
diff --git a/modules/imaging_browser/README.md b/modules/imaging_browser/README.md
new file mode 100644
index 00000000000..82e585a5a9d
--- /dev/null
+++ b/modules/imaging_browser/README.md
@@ -0,0 +1,84 @@
+# Imaging Browser
+
+## Purpose
+
+The imaging browser is intended to allow users to view candidate
+scan sessions collected for a study.
+
+## Intended Users
+
+The three primary types of users are:
+1. Imaging specialists using the modules to do online QC
+2. Project radiologists viewing images to report incidental findings
+3. Site coordinators or researchers ensuring their uploaded scans have
+ been processed and inserted into LORIS.
+
+## Scope
+
+The imaging browser displays processed images which meet the study's
+inclusion criteria. The inclusion criteria (for most images) is defined
+and enforced in the LORIS imaging pipeline scripts. Derived or
+processed scan types are also included and have their own insertion
+criteria.
+
+NOT in scope:
+
+The imaging browser module does not display raw DICOMs or perform automated
+quality control on images. It only displays images that have already been
+inserted into LORIS.
+
+## Permissions
+
+The imaging browser module uses the following permissions. Any one of them
+(except imaging_browser_qc) is sufficient to have access to the module.
+
+imaging_browser_view_allsites
+ - This permission gives the user access to all scans in the database
+
+imaging_browser_view_site
+ - This permission gives the user access to scans from their own site(s) only
+
+imaging_browser_phantom_allsites
+ - This permission gives the user access to phantom, but not candidate, scans
+ across the database
+
+imaging_browser_phantom_ownsite
+ - This permission gives the user access to phantom, but not candidate, data
+ at the user's site(s).
+
+imaging_browser_qc
+ - This permission gives the user access to modify the quality control data
+ for the associated scans and timepoints.
+
+## Configurations
+
+The imaging browser has the following configurations that affect its usage
+
+tblScanTypes - This setting determines which scan types are considered "NEW" for
+ QC purposes. It also determines which modalities are displayed on the
+ main imaging browser menu page.
+
+ImagingBrowserLinkedInstruments - This setting defines which instruments to
+ include a link to on the viewsession page.
+
+useProjects - This setting determines whether "project" filtering dropdowns exist
+ on the menu page.
+
+useEDC - This setting determines whether "EDC" filtering dropdowns exist
+ on the menu page.
+
+mantis_url - This setting defines a URL for LORIS to include a link to for bug reporting
+ on the viewsession page.
+
+## Interactions with LORIS
+
+- The "Selected" set by the imaging QC specialist is used by the dataquery
+ module in order to determine which scan to insert when multiple scans of
+ a modality type exist for a given session. (The importer exists in
+ `$LORIS/tools/CouchDB_Import_MRI.php` alongside all other CouchDB
+ import scripts, but should logically be considered part of this module.)
+- The imaging browser module includes links to BrainBrowser to visualize data.
+- The control panel on the viewsession page includes links to the instruments
+ as configured by the study.
+- The control panel on the viewsession page includes links to the DICOM Archive
+ for any DICOM tars associated with the given session.
diff --git a/modules/imaging_browser/css/imaging_browser.css b/modules/imaging_browser/css/imaging_browser.css
index 57b83f5132c..9ab31655df5 100644
--- a/modules/imaging_browser/css/imaging_browser.css
+++ b/modules/imaging_browser/css/imaging_browser.css
@@ -49,6 +49,7 @@ dl {
.div-controlpanel-bottom {
margin-left: 12px;
+ padding-bottom: 50px;
}
.dt-qc-status {
@@ -118,3 +119,7 @@ h3 {
color: #064785;
text-decoration: none;
}
+
+.tooltip-inner {
+ max-width: none;
+}
diff --git a/modules/imaging_browser/js/ImagePanel.js b/modules/imaging_browser/js/ImagePanel.js
index 76113c70bf3..c5fb545a1f7 100644
--- a/modules/imaging_browser/js/ImagePanel.js
+++ b/modules/imaging_browser/js/ImagePanel.js
@@ -1,2 +1,2 @@
-!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var ImagePanelHeader=React.createClass({displayName:"ImagePanelHeader",mixins:[React.addons.PureRenderMixin],render:function(){var QCStatusLabel;"Pass"===this.props.QCStatus?QCStatusLabel=React.createElement("span",{className:"label label-success"},this.props.QCStatus):"Fail"===this.props.QCStatus&&(QCStatusLabel=React.createElement("span",{className:"label label-danger"},this.props.QCStatus));var arrow;arrow=this.props.Expanded?React.createElement("span",{onClick:this.props.onToggleBody,className:"pull-right clickable glyphicon arrow glyphicon-chevron-up"}):React.createElement("span",{onClick:this.props.onToggleBody,className:"pull-right clickable glyphicon arrow glyphicon-chevron-down"});var headerButton=React.createElement("div",{className:"pull-right"},React.createElement("div",{className:"btn-group views"},React.createElement("button",{type:"button",className:"btn btn-default btn-xs dropdown-toggle",onClick:this.props.onToggleHeaders,"aria-expanded":this.props.HeadersExpanded},"Header Info"),React.createElement("span",{className:"caret"})));return React.createElement("div",{className:"panel-heading clearfix"},React.createElement("input",{type:"checkbox","data-file-id":this.props.FileID,className:"mripanel user-success"}),React.createElement("h3",{className:"panel-title"},this.props.Filename," "),QCStatusLabel,arrow,headerButton)}}),ImagePanelHeadersTable=React.createClass({displayName:"ImagePanelHeadersTable",componentDidMount:function(){$(ReactDOM.findDOMNode(this)).DynamicTable()},componentWillUnmount:function(){$(ReactDOM.findDOMNode(this)).DynamicTable({removeDynamicTable:!0})},render:function(){return React.createElement("table",{className:"table table-hover table-bordered header-info col-xs-12 dynamictable"},React.createElement("tbody",null,React.createElement("tr",null,React.createElement("th",{className:"info col-xs-2"},"Voxel Size"),React.createElement("td",{className:"col-xs-6",colSpan:"3"},""===this.props.HeaderInfo.XStep?" ":"X: "+this.props.HeaderInfo.XStep+" mm ",""===this.props.HeaderInfo.YStep?" ":"Y: "+this.props.HeaderInfo.YStep+" mm ",""===this.props.HeaderInfo.ZStep?" ":"Z: "+this.props.HeaderInfo.ZStep+" mm "),React.createElement("th",{className:"col-xs-2 info"},"Output Type"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.OutputType)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Acquisition Date"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.AcquisitionDate),React.createElement("th",{className:"col-xs-2 info"},"Space"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.CoordinateSpace),React.createElement("th",{className:"col-xs-2 info"},"Inserted Date"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.InsertedDate)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Protocol"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.AcquisitionProtocol),React.createElement("th",{className:"col-xs-2 info"},"Series Description"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SeriesDescription),React.createElement("th",{className:"col-xs-2 info"},"Series Number"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SeriesNumber)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Echo Time"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.EchoTime," ms"),React.createElement("th",{className:"col-xs-2 info"},"Rep Time"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.RepetitionTime," ms"),React.createElement("th",{className:"col-xs-2 info"},"Slice Thick"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SliceThickness," mm")),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Number of volumes"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.NumVolumes," volumes"),React.createElement("th",{className:"col-xs-2 info"},"Pipeline"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.Pipeline),React.createElement("th",{className:"col-xs-2 info"},"Algorithm"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.Algorithm)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Number of rejected directions"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.TotalRejected),React.createElement("th",{className:"col-xs-2 info"},"Number of Interlace correlations"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.InterlaceRejected),React.createElement("th",{className:"col-xs-2 info"},"Number of Gradient-wise correlations"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.IntergradientRejected)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Number of Slicewise correlations"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SlicewiseRejected),React.createElement("th",{className:"col-xs-2 info"},"Series Instance UID"),React.createElement("td",{className:"col-xs-2",colSpan:"2"},this.props.HeaderInfo.SeriesUID),React.createElement("td",{className:"col-xs-4",colSpan:"4"}," "))))}}),ImageQCDropdown=React.createClass({displayName:"ImageQCDropdown",render:function(){var label=React.createElement("label",null,this.props.Label);this.props.url&&(label=React.createElement("label",null,React.createElement("a",{href:this.props.url},this.props.Label)));var dropdown;if(this.props.editable){var options=[];for(var key in this.props.options)this.props.options.hasOwnProperty(key)&&options.push(React.createElement("option",{key:this.props.FormName+this.props.FileID+key,className:"form-control input-sm option",value:key},this.props.options[key]));dropdown=React.createElement("select",{name:this.props.FormName+"["+this.props.FileID+"]",defaultValue:this.props.defaultValue,className:"form-control input-sm"},options)}else dropdown=React.createElement("div",{className:"col-xs-12"},this.props.defaultValue);return React.createElement("div",{className:"row"},label,dropdown)}}),ImageQCStatic=React.createClass({displayName:"ImageQCStatic",render:function(){var staticInfo;return staticInfo=React.createElement("div",{className:"col-xs-12"},this.props.defaultValue),React.createElement("div",{className:"row"},React.createElement("label",null,this.props.Label),staticInfo)}}),ImagePanelQCStatusSelector=React.createClass({displayName:"ImagePanelQCStatusSelector",render:function(){var qcStatusLabel;return qcStatusLabel=this.props.HasQCPerm&&this.props.FileNew?React.createElement("span",null,"QC Status ",React.createElement("span",{className:"text-info"},"( ",React.createElement("span",{className:"glyphicon glyphicon-star"})," New )")):"QC Status",React.createElement(ImageQCDropdown,{Label:qcStatusLabel,FormName:"status",FileID:this.props.FileID,editable:this.props.HasQCPerm,defaultValue:this.props.QCStatus,options:{"":"",Pass:"Pass",Fail:"Fail"}})}}),ImagePanelQCSelectedSelector=React.createClass({displayName:"ImagePanelQCSelectedSelector",render:function(){return React.createElement(ImageQCDropdown,{Label:"Selected",FormName:"selectedvol",FileID:this.props.FileID,editable:this.props.HasQCPerm,options:{"":"",true:"True",false:"False"},defaultValue:this.props.Selected})}}),ImagePanelQCCaveatSelector=React.createClass({displayName:"ImagePanelQCCaveatSelector",render:function(){var mriViolationsLink=null;return this.props.SeriesUID&&"1"===this.props.Caveat&&(mriViolationsLink="/mri_violations/?submenu=mri_protocol_check_violations&SeriesUID="+this.props.SeriesUID+"&filter=true"),React.createElement(ImageQCDropdown,{Label:"Caveat",FormName:"caveat",FileID:this.props.FileID,editable:this.props.HasQCPerm,options:{"":"",1:"True",0:"False"},defaultValue:this.props.Caveat,url:mriViolationsLink})}}),ImagePanelQCSNRValue=React.createClass({displayName:"ImagePanelQCSNRValue",render:function(){return React.createElement(ImageQCStatic,{Label:"SNR",FormName:"snr",FileID:this.props.FileID,defaultValue:this.props.SNR})}}),ImagePanelQCPanel=React.createClass({displayName:"ImagePanelQCPanel",mixins:[React.addons.PureRenderMixin],render:function(){return React.createElement("div",{className:"form-group"},React.createElement(ImagePanelQCStatusSelector,{FileID:this.props.FileID,HasQCPerm:this.props.HasQCPerm,QCStatus:this.props.QCStatus,FileNew:this.props.FileNew}),React.createElement(ImagePanelQCSelectedSelector,{FileID:this.props.FileID,HasQCPerm:this.props.HasQCPerm,Selected:this.props.Selected}),React.createElement(ImagePanelQCCaveatSelector,{FileID:this.props.FileID,HasQCPerm:this.props.HasQCPerm,Caveat:this.props.Caveat,SeriesUID:this.props.SeriesUID}),React.createElement(ImagePanelQCSNRValue,{FileID:this.props.FileID,SNR:this.props.SNR}))}}),DownloadButton=React.createClass({displayName:"DownloadButton",render:function(){if(!this.props.FileName||""===this.props.FileName)return React.createElement("span",null);var style={margin:6};return React.createElement("a",{href:this.props.BaseURL+"/mri/jiv/get_file.php?file="+this.props.FileName,className:"btn btn-default",style:style},React.createElement("span",{className:"glyphicon glyphicon-download-alt"}),React.createElement("span",{className:"hidden-xs"},this.props.Label))}}),ImageQCCommentsButton=React.createClass({displayName:"ImageQCCommentsButton",openWindowHandler:function(e){e.preventDefault(),window.open(this.props.BaseURL+"/feedback_mri_popup.php?fileID="+this.props.FileID,"feedback_mri","width=500,height=800,toolbar=no,location=no,status=yes,scrollbars=yes,resizable=yes")},render:function(){return this.props.FileID&&""!==this.props.FileID?React.createElement("a",{className:"btn btn-default",href:"#noID",onClick:this.openWindowHandler},React.createElement("span",{className:"text-default"},React.createElement("span",{className:"glyphicon glyphicon-pencil"}),React.createElement("span",{className:"hidden-xs"},"QC Comments"))):React.createElement("span",null)}}),LongitudinalViewButton=React.createClass({displayName:"LongitudinalViewButton",openWindowHandler:function(e){e.preventDefault(),window.open(this.props.BaseURL+"/brainbrowser/?minc_id=["+this.props.OtherTimepoints+"]","BrainBrowser Volume Viewer","location = 0,width = auto, height = auto, scrollbars=yes")},render:function(){return this.props.FileID&&""!==this.props.FileID?React.createElement("a",{className:"btn btn-default",href:"#noID",onClick:this.openWindowHandler},React.createElement("span",{className:"text-default"},React.createElement("span",{className:"glyphicon glyphicon-eye-open"}),React.createElement("span",{className:"hidden-xs"},"Longitudinal View"))):React.createElement("span",null)}}),ImageDownloadButtons=React.createClass({displayName:"ImageDownloadButtons",render:function(){return React.createElement("div",{className:"row mri-second-row-panel col-xs-12"},React.createElement(ImageQCCommentsButton,{FileID:this.props.FileID,BaseURL:this.props.BaseURL}),React.createElement(DownloadButton,{FileName:this.props.Fullname,Label:"Download Minc",BaseURL:this.props.BaseURL}),React.createElement(DownloadButton,{FileName:this.props.XMLProtocol,BaseURL:this.props.BaseURL,Label:"Download XML Protocol"}),React.createElement(DownloadButton,{FileName:this.props.XMLReport,BaseURL:this.props.BaseURL,Label:"Download XML Report"}),React.createElement(DownloadButton,{FileName:this.props.NrrdFile,BaseURL:this.props.BaseURL,Label:"Download NRRD"}),React.createElement(LongitudinalViewButton,{FileID:this.props.FileID,BaseURL:this.props.BaseURL,OtherTimepoints:this.props.OtherTimepoints}))}}),ImagePanelBody=React.createClass({displayName:"ImagePanelBody",mixins:[React.addons.PureRenderMixin],openWindowHandler:function(e){e.preventDefault(),window.open(this.props.BaseURL+"/brainbrowser/?minc_id=["+this.props.FileID+"]","BrainBrowser Volume Viewer","location = 0,width = auto, height = auto, scrollbars=yes")},render:function(){return React.createElement("div",{className:"panel-body"},React.createElement("div",{className:"row"},React.createElement("div",{className:"col-xs-9 imaging_browser_pic"},React.createElement("a",{href:"#noID",onClick:this.openWindowHandler},React.createElement("img",{className:"img-checkpic img-responsive",src:this.props.Checkpic}))),React.createElement("div",{className:"col-xs-3 mri-right-panel"},React.createElement(ImagePanelQCPanel,{FileID:this.props.FileID,FileNew:this.props.FileNew,HasQCPerm:this.props.HasQCPerm,QCStatus:this.props.QCStatus,Caveat:this.props.Caveat,Selected:this.props.Selected,SNR:this.props.SNR,SeriesUID:this.props.SeriesUID}))),React.createElement(ImageDownloadButtons,{BaseURL:this.props.BaseURL,FileID:this.props.FileID,Fullname:this.props.Fullname,XMLProtocol:this.props.XMLProtocol,XMLReport:this.props.XMLReport,NrrdFile:this.props.NrrdFile,OtherTimepoints:this.props.OtherTimepoints}),this.props.HeadersExpanded?React.createElement(ImagePanelHeadersTable,{HeaderInfo:this.props.HeaderInfo}):"")}}),ImagePanel=React.createClass({displayName:"ImagePanel",getInitialState:function(){return{BodyCollapsed:!1,HeadersCollapsed:!0}},toggleBody:function(e){this.setState({BodyCollapsed:!this.state.BodyCollapsed})},toggleHeaders:function(e){this.setState({HeadersCollapsed:!this.state.HeadersCollapsed})},render:function(){return React.createElement("div",{className:"col-xs-12 col-md-6"},React.createElement("div",{className:"panel panel-default"},React.createElement(ImagePanelHeader,{FileID:this.props.FileID,Filename:this.props.Filename,QCStatus:this.props.QCStatus,onToggleBody:this.toggleBody,onToggleHeaders:this.toggleHeaders,Expanded:!this.state.BodyCollapsed,HeadersExpanded:!this.state.HeadersCollapsed}),this.state.BodyCollapsed?"":React.createElement(ImagePanelBody,{BaseURL:this.props.BaseURL,FileID:this.props.FileID,Filename:this.props.Filename,Checkpic:this.props.Checkpic,HeadersExpanded:!this.state.HeadersCollapsed,HeaderInfo:this.props.HeaderInfo,FileNew:this.props.FileNew,HasQCPerm:this.props.HasQCPerm,QCStatus:this.props.QCStatus,Caveat:this.props.Caveat,Selected:this.props.Selected,SNR:this.props.SNR,Fullname:this.props.Fullname,XMLProtocol:this.props.XMLProtocol,XMLReport:this.props.XMLReport,NrrdFile:this.props.NrrdFile,OtherTimepoints:this.props.OtherTimepoints,SeriesUID:this.props.SeriesUID})))}}),RImagePanel=React.createFactory(ImagePanel);window.ImagePanelHeader=ImagePanelHeader,window.ImagePanelHeadersTable=ImagePanelHeadersTable,window.ImageQCDropdown=ImageQCDropdown,window.ImageQCStatic=ImageQCStatic,window.ImagePanelQCStatusSelector=ImagePanelQCStatusSelector,window.ImagePanelQCSelectedSelector=ImagePanelQCSelectedSelector,window.ImagePanelQCCaveatSelector=ImagePanelQCCaveatSelector,window.ImagePanelQCSNRValue=ImagePanelQCSNRValue,window.ImagePanelQCPanel=ImagePanelQCPanel,window.DownloadButton=DownloadButton,window.ImageQCCommentsButton=ImageQCCommentsButton,window.LongitudinalViewButton=LongitudinalViewButton,window.ImageDownloadButtons=ImageDownloadButtons,window.ImagePanelBody=ImagePanelBody,window.RImagePanel=RImagePanel,exports.default={ImagePanelHeader:ImagePanelHeader,ImagePanelHeadersTable:ImagePanelHeadersTable,ImageQCDropdown:ImageQCDropdown,ImageQCStatic:ImageQCStatic,ImagePanelQCStatusSelector:ImagePanelQCStatusSelector,ImagePanelQCSelectedSelector:ImagePanelQCSelectedSelector,ImagePanelQCCaveatSelector:ImagePanelQCCaveatSelector,ImagePanelQCSNRValue:ImagePanelQCSNRValue,ImagePanelQCPanel:ImagePanelQCPanel,DownloadButton:DownloadButton,ImageQCCommentsButton:ImageQCCommentsButton,LongitudinalViewButton:LongitudinalViewButton,ImageDownloadButtons:ImageDownloadButtons,ImagePanelBody:ImagePanelBody,RImagePanel:RImagePanel}}]);
+!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var ImagePanelHeader=React.createClass({displayName:"ImagePanelHeader",mixins:[React.addons.PureRenderMixin],componentDidMount:function(){$(".panel-title").tooltip()},render:function(){var QCStatusLabel;"Pass"===this.props.QCStatus?QCStatusLabel=React.createElement("span",{className:"label label-success"},this.props.QCStatus):"Fail"===this.props.QCStatus&&(QCStatusLabel=React.createElement("span",{className:"label label-danger"},this.props.QCStatus));var arrow;arrow=this.props.Expanded?React.createElement("span",{onClick:this.props.onToggleBody,className:"pull-right clickable glyphicon arrow glyphicon-chevron-up"}):React.createElement("span",{onClick:this.props.onToggleBody,className:"pull-right clickable glyphicon arrow glyphicon-chevron-down"});var headerButton=React.createElement("div",{className:"pull-right"},React.createElement("div",{className:"btn-group views"},React.createElement("button",{type:"button",className:"btn btn-default btn-xs dropdown-toggle",onClick:this.props.onToggleHeaders,"aria-expanded":this.props.HeadersExpanded},"Header Info"),React.createElement("span",{className:"caret"})));return React.createElement("div",{className:"panel-heading clearfix"},React.createElement("input",{type:"checkbox","data-file-id":this.props.FileID,className:"mripanel user-success"}),React.createElement("h3",{className:"panel-title","data-toggle":"tooltip",title:this.props.Filename},this.props.Filename),QCStatusLabel,arrow,headerButton)}}),ImagePanelHeadersTable=React.createClass({displayName:"ImagePanelHeadersTable",componentDidMount:function(){$(ReactDOM.findDOMNode(this)).DynamicTable()},componentWillUnmount:function(){$(ReactDOM.findDOMNode(this)).DynamicTable({removeDynamicTable:!0})},render:function(){return React.createElement("table",{className:"table table-hover table-bordered header-info col-xs-12 dynamictable"},React.createElement("tbody",null,React.createElement("tr",null,React.createElement("th",{className:"info col-xs-2"},"Voxel Size"),React.createElement("td",{className:"col-xs-6",colSpan:"3"},""===this.props.HeaderInfo.XStep?" ":"X: "+this.props.HeaderInfo.XStep+" mm ",""===this.props.HeaderInfo.YStep?" ":"Y: "+this.props.HeaderInfo.YStep+" mm ",""===this.props.HeaderInfo.ZStep?" ":"Z: "+this.props.HeaderInfo.ZStep+" mm "),React.createElement("th",{className:"col-xs-2 info"},"Output Type"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.OutputType)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Acquisition Date"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.AcquisitionDate),React.createElement("th",{className:"col-xs-2 info"},"Space"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.CoordinateSpace),React.createElement("th",{className:"col-xs-2 info"},"Inserted Date"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.InsertedDate)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Protocol"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.AcquisitionProtocol),React.createElement("th",{className:"col-xs-2 info"},"Series Description"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SeriesDescription),React.createElement("th",{className:"col-xs-2 info"},"Series Number"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SeriesNumber)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Echo Time"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.EchoTime," ms"),React.createElement("th",{className:"col-xs-2 info"},"Rep Time"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.RepetitionTime," ms"),React.createElement("th",{className:"col-xs-2 info"},"Slice Thick"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SliceThickness," mm")),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Number of volumes"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.NumVolumes," volumes"),React.createElement("th",{className:"col-xs-2 info"},"Pipeline"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.Pipeline),React.createElement("th",{className:"col-xs-2 info"},"Algorithm"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.Algorithm)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Number of rejected directions"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.TotalRejected),React.createElement("th",{className:"col-xs-2 info"},"Number of Interlace correlations"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.InterlaceRejected),React.createElement("th",{className:"col-xs-2 info"},"Number of Gradient-wise correlations"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.IntergradientRejected)),React.createElement("tr",null,React.createElement("th",{className:"col-xs-2 info"},"Number of Slicewise correlations"),React.createElement("td",{className:"col-xs-2"},this.props.HeaderInfo.SlicewiseRejected),React.createElement("th",{className:"col-xs-2 info"},"Series Instance UID"),React.createElement("td",{className:"col-xs-2",colSpan:"2"},this.props.HeaderInfo.SeriesUID),React.createElement("td",{className:"col-xs-4",colSpan:"4"}," "))))}}),ImageQCDropdown=React.createClass({displayName:"ImageQCDropdown",render:function(){var label=React.createElement("label",null,this.props.Label);this.props.url&&(label=React.createElement("label",null,React.createElement("a",{href:this.props.url},this.props.Label)));var dropdown;if(this.props.editable){var options=[];for(var key in this.props.options)this.props.options.hasOwnProperty(key)&&options.push(React.createElement("option",{key:this.props.FormName+this.props.FileID+key,className:"form-control input-sm option",value:key},this.props.options[key]));dropdown=React.createElement("select",{name:this.props.FormName+"["+this.props.FileID+"]",defaultValue:this.props.defaultValue,className:"form-control input-sm"},options)}else dropdown=React.createElement("div",{className:"col-xs-12"},this.props.defaultValue);return React.createElement("div",{className:"row"},label,dropdown)}}),ImageQCStatic=React.createClass({displayName:"ImageQCStatic",render:function(){var staticInfo;return staticInfo=React.createElement("div",{className:"col-xs-12"},this.props.defaultValue),React.createElement("div",{className:"row"},React.createElement("label",null,this.props.Label),staticInfo)}}),ImagePanelQCStatusSelector=React.createClass({displayName:"ImagePanelQCStatusSelector",render:function(){var qcStatusLabel;return qcStatusLabel=this.props.HasQCPerm&&this.props.FileNew?React.createElement("span",null,"QC Status ",React.createElement("span",{className:"text-info"},"( ",React.createElement("span",{className:"glyphicon glyphicon-star"})," New )")):"QC Status",React.createElement(ImageQCDropdown,{Label:qcStatusLabel,FormName:"status",FileID:this.props.FileID,editable:this.props.HasQCPerm,defaultValue:this.props.QCStatus,options:{"":"",Pass:"Pass",Fail:"Fail"}})}}),ImagePanelQCSelectedSelector=React.createClass({displayName:"ImagePanelQCSelectedSelector",render:function(){return React.createElement(ImageQCDropdown,{Label:"Selected",FormName:"selectedvol",FileID:this.props.FileID,editable:this.props.HasQCPerm,options:{"":"",true:"True",false:"False"},defaultValue:this.props.Selected})}}),ImagePanelQCCaveatSelector=React.createClass({displayName:"ImagePanelQCCaveatSelector",render:function(){var mriViolationsLink=null;return this.props.SeriesUID&&"1"===this.props.Caveat&&(mriViolationsLink="/mri_violations/?submenu=mri_protocol_check_violations&SeriesUID="+this.props.SeriesUID+"&filter=true"),React.createElement(ImageQCDropdown,{Label:"Caveat",FormName:"caveat",FileID:this.props.FileID,editable:this.props.HasQCPerm,options:{"":"",1:"True",0:"False"},defaultValue:this.props.Caveat,url:mriViolationsLink})}}),ImagePanelQCSNRValue=React.createClass({displayName:"ImagePanelQCSNRValue",render:function(){return React.createElement(ImageQCStatic,{Label:"SNR",FormName:"snr",FileID:this.props.FileID,defaultValue:this.props.SNR})}}),ImagePanelQCPanel=React.createClass({displayName:"ImagePanelQCPanel",mixins:[React.addons.PureRenderMixin],render:function(){return React.createElement("div",{className:"form-group"},React.createElement(ImagePanelQCStatusSelector,{FileID:this.props.FileID,HasQCPerm:this.props.HasQCPerm,QCStatus:this.props.QCStatus,FileNew:this.props.FileNew}),React.createElement(ImagePanelQCSelectedSelector,{FileID:this.props.FileID,HasQCPerm:this.props.HasQCPerm,Selected:this.props.Selected}),React.createElement(ImagePanelQCCaveatSelector,{FileID:this.props.FileID,HasQCPerm:this.props.HasQCPerm,Caveat:this.props.Caveat,SeriesUID:this.props.SeriesUID}),React.createElement(ImagePanelQCSNRValue,{FileID:this.props.FileID,SNR:this.props.SNR}))}}),DownloadButton=React.createClass({displayName:"DownloadButton",render:function(){if(!this.props.FileName||""===this.props.FileName)return React.createElement("span",null);var style={margin:6};return React.createElement("a",{href:this.props.BaseURL+"/mri/jiv/get_file.php?file="+this.props.FileName,className:"btn btn-default",style:style},React.createElement("span",{className:"glyphicon glyphicon-download-alt"}),React.createElement("span",{className:"hidden-xs"},this.props.Label))}}),ImageQCCommentsButton=React.createClass({displayName:"ImageQCCommentsButton",openWindowHandler:function(e){e.preventDefault(),window.open(this.props.BaseURL+"/feedback_mri_popup.php?fileID="+this.props.FileID,"feedback_mri","width=500,height=800,toolbar=no,location=no,status=yes,scrollbars=yes,resizable=yes")},render:function(){return this.props.FileID&&""!==this.props.FileID?React.createElement("a",{className:"btn btn-default",href:"#noID",onClick:this.openWindowHandler},React.createElement("span",{className:"text-default"},React.createElement("span",{className:"glyphicon glyphicon-pencil"}),React.createElement("span",{className:"hidden-xs"},"QC Comments"))):React.createElement("span",null)}}),LongitudinalViewButton=React.createClass({displayName:"LongitudinalViewButton",openWindowHandler:function(e){e.preventDefault(),window.open(this.props.BaseURL+"/brainbrowser/?minc_id=["+this.props.OtherTimepoints+"]","BrainBrowser Volume Viewer","location = 0,width = auto, height = auto, scrollbars=yes")},render:function(){return this.props.FileID&&""!==this.props.FileID?React.createElement("a",{className:"btn btn-default",href:"#noID",onClick:this.openWindowHandler},React.createElement("span",{className:"text-default"},React.createElement("span",{className:"glyphicon glyphicon-eye-open"}),React.createElement("span",{className:"hidden-xs"},"Longitudinal View"))):React.createElement("span",null)}}),ImageDownloadButtons=React.createClass({displayName:"ImageDownloadButtons",render:function(){return React.createElement("div",{className:"row mri-second-row-panel col-xs-12"},React.createElement(ImageQCCommentsButton,{FileID:this.props.FileID,BaseURL:this.props.BaseURL}),React.createElement(DownloadButton,{FileName:this.props.Fullname,Label:"Download Minc",BaseURL:this.props.BaseURL}),React.createElement(DownloadButton,{FileName:this.props.XMLProtocol,BaseURL:this.props.BaseURL,Label:"Download XML Protocol"}),React.createElement(DownloadButton,{FileName:this.props.XMLReport,BaseURL:this.props.BaseURL,Label:"Download XML Report"}),React.createElement(DownloadButton,{FileName:this.props.NrrdFile,BaseURL:this.props.BaseURL,Label:"Download NRRD"}),React.createElement(LongitudinalViewButton,{FileID:this.props.FileID,BaseURL:this.props.BaseURL,OtherTimepoints:this.props.OtherTimepoints}))}}),ImagePanelBody=React.createClass({displayName:"ImagePanelBody",mixins:[React.addons.PureRenderMixin],openWindowHandler:function(e){e.preventDefault(),window.open(this.props.BaseURL+"/brainbrowser/?minc_id=["+this.props.FileID+"]","BrainBrowser Volume Viewer","location = 0,width = auto, height = auto, scrollbars=yes")},render:function(){return React.createElement("div",{className:"panel-body"},React.createElement("div",{className:"row"},React.createElement("div",{className:"col-xs-9 imaging_browser_pic"},React.createElement("a",{href:"#noID",onClick:this.openWindowHandler},React.createElement("img",{className:"img-checkpic img-responsive",src:this.props.Checkpic}))),React.createElement("div",{className:"col-xs-3 mri-right-panel"},React.createElement(ImagePanelQCPanel,{FileID:this.props.FileID,FileNew:this.props.FileNew,HasQCPerm:this.props.HasQCPerm,QCStatus:this.props.QCStatus,Caveat:this.props.Caveat,Selected:this.props.Selected,SNR:this.props.SNR,SeriesUID:this.props.SeriesUID}))),React.createElement(ImageDownloadButtons,{BaseURL:this.props.BaseURL,FileID:this.props.FileID,Fullname:this.props.Fullname,XMLProtocol:this.props.XMLProtocol,XMLReport:this.props.XMLReport,NrrdFile:this.props.NrrdFile,OtherTimepoints:this.props.OtherTimepoints}),this.props.HeadersExpanded?React.createElement(ImagePanelHeadersTable,{HeaderInfo:this.props.HeaderInfo}):"")}}),ImagePanel=React.createClass({displayName:"ImagePanel",getInitialState:function(){return{BodyCollapsed:!1,HeadersCollapsed:!0}},toggleBody:function(e){this.setState({BodyCollapsed:!this.state.BodyCollapsed})},toggleHeaders:function(e){this.setState({HeadersCollapsed:!this.state.HeadersCollapsed})},render:function(){return React.createElement("div",{className:"col-xs-12 col-md-6"},React.createElement("div",{className:"panel panel-default"},React.createElement(ImagePanelHeader,{FileID:this.props.FileID,Filename:this.props.Filename,QCStatus:this.props.QCStatus,onToggleBody:this.toggleBody,onToggleHeaders:this.toggleHeaders,Expanded:!this.state.BodyCollapsed,HeadersExpanded:!this.state.HeadersCollapsed}),this.state.BodyCollapsed?"":React.createElement(ImagePanelBody,{BaseURL:this.props.BaseURL,FileID:this.props.FileID,Filename:this.props.Filename,Checkpic:this.props.Checkpic,HeadersExpanded:!this.state.HeadersCollapsed,HeaderInfo:this.props.HeaderInfo,FileNew:this.props.FileNew,HasQCPerm:this.props.HasQCPerm,QCStatus:this.props.QCStatus,Caveat:this.props.Caveat,Selected:this.props.Selected,SNR:this.props.SNR,Fullname:this.props.Fullname,XMLProtocol:this.props.XMLProtocol,XMLReport:this.props.XMLReport,NrrdFile:this.props.NrrdFile,OtherTimepoints:this.props.OtherTimepoints,SeriesUID:this.props.SeriesUID})))}}),RImagePanel=React.createFactory(ImagePanel);window.ImagePanelHeader=ImagePanelHeader,window.ImagePanelHeadersTable=ImagePanelHeadersTable,window.ImageQCDropdown=ImageQCDropdown,window.ImageQCStatic=ImageQCStatic,window.ImagePanelQCStatusSelector=ImagePanelQCStatusSelector,window.ImagePanelQCSelectedSelector=ImagePanelQCSelectedSelector,window.ImagePanelQCCaveatSelector=ImagePanelQCCaveatSelector,window.ImagePanelQCSNRValue=ImagePanelQCSNRValue,window.ImagePanelQCPanel=ImagePanelQCPanel,window.DownloadButton=DownloadButton,window.ImageQCCommentsButton=ImageQCCommentsButton,window.LongitudinalViewButton=LongitudinalViewButton,window.ImageDownloadButtons=ImageDownloadButtons,window.ImagePanelBody=ImagePanelBody,window.RImagePanel=RImagePanel,exports.default={ImagePanelHeader:ImagePanelHeader,ImagePanelHeadersTable:ImagePanelHeadersTable,ImageQCDropdown:ImageQCDropdown,ImageQCStatic:ImageQCStatic,ImagePanelQCStatusSelector:ImagePanelQCStatusSelector,ImagePanelQCSelectedSelector:ImagePanelQCSelectedSelector,ImagePanelQCCaveatSelector:ImagePanelQCCaveatSelector,ImagePanelQCSNRValue:ImagePanelQCSNRValue,ImagePanelQCPanel:ImagePanelQCPanel,DownloadButton:DownloadButton,ImageQCCommentsButton:ImageQCCommentsButton,LongitudinalViewButton:LongitudinalViewButton,ImageDownloadButtons:ImageDownloadButtons,ImagePanelBody:ImagePanelBody,RImagePanel:RImagePanel}}]);
//# sourceMappingURL=ImagePanel.js.map
\ No newline at end of file
diff --git a/modules/imaging_browser/jsx/ImagePanel.js b/modules/imaging_browser/jsx/ImagePanel.js
index 2552ddf1fc9..46cf2606c87 100644
--- a/modules/imaging_browser/jsx/ImagePanel.js
+++ b/modules/imaging_browser/jsx/ImagePanel.js
@@ -6,6 +6,9 @@
var ImagePanelHeader = React.createClass({
mixins: [React.addons.PureRenderMixin],
+ componentDidMount: function() {
+ $(".panel-title").tooltip();
+ },
render: function() {
var QCStatusLabel;
if (this.props.QCStatus === 'Pass') {
@@ -47,7 +50,9 @@ var ImagePanelHeader = React.createClass({
-
{this.props.Filename}
+
+ {this.props.Filename}
+
{QCStatusLabel}
{arrow}
{headerButton}
diff --git a/modules/imaging_browser/php/imaging_browser.class.inc b/modules/imaging_browser/php/imaging_browser.class.inc
index 33f000c87cf..3dae8895475 100644
--- a/modules/imaging_browser/php/imaging_browser.class.inc
+++ b/modules/imaging_browser/php/imaging_browser.class.inc
@@ -50,7 +50,7 @@ class Imaging_Browser extends \NDB_Menu_Filter
* Set up the variables required by NDB_Menu_Filter class for constructing
* a query
*
- * @return null
+ * @return void
*/
function _setupVariables()
{
@@ -290,7 +290,7 @@ class Imaging_Browser extends \NDB_Menu_Filter
/**
* Setup $this->tpl_data for use by Smarty
*
- * @return null
+ * @return void
*/
function setup()
{
@@ -422,7 +422,7 @@ class Imaging_Browser extends \NDB_Menu_Filter
* @param string $field filter field
* @param string $val filter value
*
- * @return null
+ * @return string the new query
*/
function _addValidFilters($prepared_key, $field, $val)
{
@@ -473,7 +473,7 @@ class Imaging_Browser extends \NDB_Menu_Filter
* will be used for the Navigation Links in the viewSession
* page.
*
- * @return associative array
+ * @return array
*/
function toArray()
{
diff --git a/modules/imaging_browser/php/imaging_session_controlpanel.class.inc b/modules/imaging_browser/php/imaging_session_controlpanel.class.inc
index 6a1e88e73b0..40db45ffa1e 100644
--- a/modules/imaging_browser/php/imaging_session_controlpanel.class.inc
+++ b/modules/imaging_browser/php/imaging_session_controlpanel.class.inc
@@ -63,25 +63,34 @@ class Imaging_Session_ControlPanel
function getData()
{
$DB = \Database::singleton();
+ $config = \NDB_Config::singleton();
$timePoint =& \TimePoint::singleton($_REQUEST['sessionID']);
$subjectData['sessionID'] = $_REQUEST['sessionID'];
$subjectData['candid'] = $timePoint->getCandID();
- $qresult = $DB->pselectOne(
- "SELECT CommentID FROM flag
- WHERE Test_name='mri_parameter_form' AND SessionID = :v_sessions_id",
- array('v_sessions_id' => $this->sessionID)
- );
- $subjectData['ParameterFormCommentID'] = (empty($qresult)) ? "" : $qresult;
-
- $qresult = $DB->pselectOne(
- "SELECT CommentID
- FROM flag WHERE Test_name='radiology_review'
- AND SessionID = :v_sessions_id",
- array('v_sessions_id' => $this->sessionID)
- );
- $subjectData['RadiologyReviewCommentID'] = (empty($qresult)) ? "" : $qresult;
+ $linkedInstruments
+ = $config->getSetting('ImagingBrowserLinkedInstruments');
+
+ $links = array();
+ foreach ($linkedInstruments as $k=>$v) {
+ $qresult = $DB->pselectRow(
+ "SELECT f.CommentID, tn.Full_name FROM flag f
+ JOIN test_names tn ON f.Test_name = tn.Test_name
+ WHERE f.Test_name = :tname AND f.SessionID = :v_sessions_id",
+ array(
+ 'tname' => $v,
+ 'v_sessions_id' => $this->sessionID,
+ )
+ );
+ $links[] = array(
+ "FEName" => $qresult['Full_name'],
+ "BEName" => $v,
+ "CommentID" => $qresult['CommentID'],
+ );
+ }
+ $subjectData['links']
+ = (empty($links)) ? "" : $links;
$candidate =& \Candidate::singleton($timePoint->getCandID());
@@ -97,7 +106,7 @@ class Imaging_Session_ControlPanel
$params['PVL'] = $timePoint->getVisitLabel();
}
$tarchiveIDs = $DB->pselect(
- "SELECT TarchiveID
+ "SELECT TarchiveID
FROM tarchive
WHERE PatientName LIKE $ID",
$params
diff --git a/modules/imaging_browser/php/viewsession.class.inc b/modules/imaging_browser/php/viewsession.class.inc
index 477a6be0ae1..eddb4600de1 100644
--- a/modules/imaging_browser/php/viewsession.class.inc
+++ b/modules/imaging_browser/php/viewsession.class.inc
@@ -78,7 +78,7 @@ class ViewSession extends \NDB_Form
/**
* Sets up main parameters
*
- * @return NULL
+ * @return void
*/
function setup()
{
@@ -740,7 +740,7 @@ class ViewSession extends \NDB_Form
/**
* Get js Dependencies
*
- * @return string
+ * @return array
*/
function getJSDependencies()
{
diff --git a/modules/imaging_browser/templates/imaging_session_controlpanel.tpl b/modules/imaging_browser/templates/imaging_session_controlpanel.tpl
index 7a502faff24..69bbac02478 100644
--- a/modules/imaging_browser/templates/imaging_session_controlpanel.tpl
+++ b/modules/imaging_browser/templates/imaging_session_controlpanel.tpl
@@ -32,12 +32,9 @@
Links
- {if $mri_param_form_table_exists}
- MRI Parameter Form
- {/if}
- {if $rad_review_table_exists}
- Radiology Review
- {/if}
+ {foreach from=$subject.links item=link}
+ {$link.FEName}
+ {/foreach}
{foreach from=$subject.tarchiveids item=tarchive}
DICOM Archive {$tarchive.TarchiveID} {/foreach}
{if $issue_tracker_url}
diff --git a/modules/imaging_browser/test/imaging_browser_test_plan.md b/modules/imaging_browser/test/imaging_browser_test_plan.md
index 66134569eba..28cded7e1d6 100644
--- a/modules/imaging_browser/test/imaging_browser_test_plan.md
+++ b/modules/imaging_browser/test/imaging_browser_test_plan.md
@@ -11,7 +11,7 @@
### B. ViewSession / Volume List
-8. Sidebar: all links work
+8. Sidebar: all links work. Ensure that projects can customize (add/remove) their list of instruments that can be linked to from the Configuration/Imaging Modules/Imaging Browser Links to Insruments
9. 3d panel overlay etc - they work. Add panel checkbox works. 3D only or 3D+Overlay loads files if at least one image exists and is selected
10. "Visit Level Feedback" - pops up QC window (see section E below)
11. Visit level QC controls (Pass/Fail, Pending, Visit Level Caveat) viewable to all, editable IFF permission imaging_browser_qc
diff --git a/modules/imaging_uploader/README.md b/modules/imaging_uploader/README.md
index 3f66e169733..dc94a41544d 100644
--- a/modules/imaging_uploader/README.md
+++ b/modules/imaging_uploader/README.md
@@ -1,14 +1,106 @@
-To enable the Imaging Uploader to handle large files, please update the apache configuration file
-with the following values.
+# Imaging Uploader
-File to update : `/etc/php5/apache2/php.ini`
+## Purpose
-Sample values:
+The imaging uploader is intended to allow users to upload, browse, and track
+pipeline insertion progress.
+
+
+## Intended Users
+
+The primary users are MRI technicians or site coordinators uploading imaging
+scans for registered LORIS candidates and timepoints.
+
+## Scope
+
+The imaging uploader has the following built-in capabilities to facilitate
+timely scan insertion into the LORIS database. Specifically, it allows to browse
+uploaded scans using the `Browse` tab, upload imaging scans using the `Upload`
+tab, and track scan insertion status through the `Progress` column and a
+`Log Viewer` which displays in either `Summary` or `Detailed` mode relevant
+feedback messages about insertion success or failure causes.
+
+Uploaded scans can be overwritten if insertion status is `Not Started` or
+`Failed` (i.e. if the `Progress` is neither `In Progress` nor `Success`).
+
+
+NOT in scope:
+
+The imaging uploader does NOT read the DICOM files within the uploaded scans.
+As such, it does not check if the files within the uploaded scan are of the
+expected DICOM type, nor whether the PatientName and/or PatientID DICOM headers
+are properly de-identified according to the LORIS convention. This check is
+however done as the first step on the LORIS-MRI side; i.e. as soon as the
+insertion pipeline is triggered.
+
+## Requirements
+
+For a successful upload:
+- The uploaded file is expected to be of one of the following types:
+`.tgz`, `.tar.gz` or `.zip`.
+- The filename should follow the:
+`PSCID_CandID_VisitLabel_OptionalSuffix` naming convention
+- It is expected that the candidate and timepoint are already created in the
+database.
+
+
+## Permissions
+
+#### Module Permission
+
+The imaging uploader module uses one permission called `imaging_uploader` that
+is necessary to have access to the module and gives the user the ability to
+upload and browse all scans uploaded to the database.
+
+#### Filesystem Permission
+
+The path on the filesystem where the uploaded file go
+(see section [Database Configuration](#database_config_link)) should be
+readable and writable by the web server. This is automatically achieved by the
+LORIS-MRI install process.
+
+
+## Configurations
+
+The imaging uploader has the following configurations that affect its usage:
+
+#### Install Configurations
+
+To enable the Imaging Uploader to handle large files, please update the
+`php.ini` apache configuration file. Recommended sample values appropriate for
+compressed scans not exceeding 500M in size are:
```
-session.gc_maxlifetime = 10800
-max_input_time = 10800
-max_execution_time = 10800
-upload_max_filesize = 1024M
-post_max_size = 1024M
+session.gc_maxlifetime = 10800 // After this number of seconds, stored data will be seen as 'garbage' and cleaned up by the garbage collection process.
+max_input_time = 10800 // Maximum amount of time each script may spend parsing request data (in seconds)
+max_execution_time = 10800 // Maximum execution time of each script (in seconds)
+upload_max_filesize = 1024M // Maximum allowed size for uploaded files.
+post_max_size = 1024M // Maximum size of POST data that PHP will accept.
```
+
+#### Database Configurations
+
+ImagingUploaderAutoLaunch - This setting determines whether the insertion
+ pipeline that archives the scans is triggered automatically or manually.
+
+MRIUploadIncomingPath - This setting determines where on the filesystem the
+ uploader is to place the uploaded scan. Default location is
+ `/data/incoming/`. This directory is created during the installation of
+ LORIS-MRI.
+ **Note**: Uploaded scans are erased from the
+ `MRIUploadIncomingPath`following a successful archival and insertion
+ through the LORIS-MRI pipeline.
+
+
+## Interactions with LORIS
+
+- The `TarchiveInfo` column links to the DICOM Archive module for that scan
+- The `Number of MincInserted` column links to the Imaging Browser module for
+that candidate's session
+- The `Number of MincCreated` column links to the MRI Violated scans module if
+violated scans (i.e. scans that violate the MRI protocol as defined by the
+study) are present
+- If `ImagingUploaderAutoLaunch` configuration is enabled, the Server Process
+Manager under the Admin menu can be consulted for scans insertion progress
+(exit codes, error files, etc...).
+
diff --git a/modules/imaging_uploader/js/index.js b/modules/imaging_uploader/js/index.js
index 62b9f6061e1..2bcec3b220d 100644
--- a/modules/imaging_uploader/js/index.js
+++ b/modules/imaging_uploader/js/index.js
@@ -1,3 +1,3 @@
-!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}var _ImagingUploader=__webpack_require__(15),_ImagingUploader2=_interopRequireDefault(_ImagingUploader);$(function(){var imagingUploader=React.createElement("div",{className:"page-imaging-uploader"},React.createElement(_ImagingUploader2.default,{Module:"imaging_uploader",DataURL:loris.BaseURL+"/imaging_uploader/?format=json"}));ReactDOM.render(imagingUploader,document.getElementById("lorisworkspace"))})},,function(module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i0&&(activeTab=_this.props.tabs[0].id),_this.state={activeTab:activeTab},_this.handleClick=_this.handleClick.bind(_this),_this.getTabs=_this.getTabs.bind(_this),_this.getTabPanes=_this.getTabPanes.bind(_this),_this}return _inherits(Tabs,_React$Component),_createClass(Tabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("ul",{className:"nav nav-tabs",role:"tablist",style:tabStyle},tabs),React.createElement("div",{className:"tab-content"},tabPanes))}}]),Tabs}(React.Component);Tabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},Tabs.defaultProps={onTabChange:function(){},updateURL:!1};var TabPane=function(_React$Component2){function TabPane(){return _classCallCheck(this,TabPane),_possibleConstructorReturn(this,(TabPane.__proto__||Object.getPrototypeOf(TabPane)).apply(this,arguments))}return _inherits(TabPane,_React$Component2),_createClass(TabPane,[{key:"render",value:function(){var classList="tab-pane",title=void 0;return this.props.TabId===this.props.activeTab&&(classList+=" active"),this.props.Title&&(title=React.createElement("h1",null,this.props.Title)),React.createElement("div",{role:"tabpanel",className:classList,id:this.props.TabId},title,this.props.children)}}]),TabPane}(React.Component);TabPane.propTypes={TabId:React.PropTypes.string.isRequired,Title:React.PropTypes.string,activeTab:React.PropTypes.string},exports.Tabs=Tabs,exports.TabPane=TabPane},,,,function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i",logType:"summary"},_this.initHelper=_this.initHelper.bind(_this),_this.onLogTypeChange=_this.onLogTypeChange.bind(_this),_this.setServerPolling=_this.setServerPolling.bind(_this),_this.monitorProgress=_this.monitorProgress.bind(_this),_this}return _inherits(LogPanel,_React$Component),_createClass(LogPanel,[{key:"componentDidMount",value:function(){this.initHelper()}},{key:"initHelper",value:function(){var uploadProgress=new UploadProgress;this.uploadProgress=uploadProgress,$("#mri_upload_table").on("click","tbody tr",function(event){return null!==uploadProgress.getUploadRow()&&($(uploadProgress.getUploadRow()).css("background-color","white"),this.setServerPolling(!1)),event.currentTarget===uploadProgress.getUploadRow()?(uploadProgress.setUploadRow(null),uploadProgress.setProgressFromServer(null),void this.setState({logText:""})):(uploadProgress.setUploadRow(event.currentTarget),$(event.currentTarget).css("background-color","#EFEFFB"),void this.monitorProgress(this.state.logType))}.bind(this))}},{key:"monitorProgress",value:function(logType){var summary="summary"===logType,uploadProgress=this.uploadProgress,uploadId=uploadProgress.getUploadId();uploadId&&$.post(loris.BaseURL+"/imaging_uploader/ajax/getUploadSummary.php",{uploadId:uploadId,summary:summary},function(data){uploadProgress.setProgressFromServer(data),this.setState({logText:uploadProgress.getProgressText()}),this.setServerPolling(uploadProgress.getPipelineStatus()===UploadProgress.PIPELINE_STATUS_RUNNING)}.bind(this))}},{key:"setServerPolling",value:function(poll){var uploadProgress=this.uploadProgress;poll?(this.setServerPolling.getSummaryInterval||(this.setServerPolling.getSummaryInterval=setInterval(this.monitorProgress,5e3)),this.setServerPolling.dotUpdateInterval||(this.setServerPolling.dotUpdateInterval=setInterval(function(){uploadProgress.updateDots(),this.setState({logText:uploadProgress.getProgressText()})},3e3)),this.setServerPolling.animatedCharInterval||(this.setServerPolling.animatedCharInterval=setInterval(function(){uploadProgress.updateAnimatedCharIndex(),this.setState({logText:uploadProgress.getProgressText()})},250))):(this.setServerPolling.getSummaryInterval&&(clearInterval(this.setServerPolling.getSummaryInterval),this.setServerPolling.getSummaryInterval=null),this.setServerPolling.dotUpdateInterval&&(clearInterval(this.setServerPolling.dotUpdateInterval),this.setServerPolling.dotUpdateInterval=null),this.setServerPolling.animatedCharInterval&&(clearInterval(this.setServerPolling.animatedCharInterval),this.setServerPolling.animatedCharInterval=null))}},{key:"onLogTypeChange",value:function(name,value){this.monitorProgress(value),this.setState({logType:value})}},{key:"render",value:function(){var logTypes={summary:"Summary",detailed:"Detailed"};return React.createElement(_Panel2.default,{id:"log_panel",title:"Log Viewer"},React.createElement(FormElement,{name:"log_form"},React.createElement(SelectElement,{name:"LogType",label:"Logs to display",options:logTypes,onUserInput:this.onLogTypeChange,value:this.state.logType,emptyOption:!1}),React.createElement(TextareaElement,{name:"UploadLogs",disabled:!0,id:"mri_upload_logs",value:this.state.logText,rows:6})))}}]),LogPanel}(React.Component);LogPanel.propTypes={},LogPanel.defaultProps={},exports.default=LogPanel},function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i-1});if(!mriFile)return void this.uploadFile();if("Success"===mriFile.status)return void swal({title:"File already exists!",text:"A file with this name has already successfully passed the MRI pipeline!\n",type:"error",confirmButtonText:"OK"});if("In Progress..."===mriFile.status)return void swal({title:"File is currently processing!",text:"A file with this name is currently going through the MRI pipeline!\n",type:"error",confirmButtonText:"OK"});"Failure"===mriFile.status&&swal({title:"Are you sure?",text:"A file with this name already exists!\n Would you like to override existing file?",type:"warning",showCancelButton:!0,confirmButtonText:"Yes, I am sure!",cancelButtonText:"No, cancel it!"},function(isConfirm){isConfirm?this.uploadFile(!0):swal("Cancelled","Your imaginary file is safe :)","error")}.bind(this)),"Not Started"===mriFile.status&&swal({title:"Are you sure?",text:"A file with this name has been uploaded but has not yet started the MRI pipeline.\n Would you like to override the existing file?",type:"warning",showCancelButton:!0,confirmButtonText:"Yes, I am sure!",cancelButtonText:"No, cancel it!"},function(isConfirm){isConfirm?this.uploadFile(!0):swal("Cancelled","Your upload has been cancelled.","error")}.bind(this))}}},{key:"uploadFile",value:function(overwriteFile){var formData=this.state.formData,formObj=new FormData;for(var key in formData)""!==formData[key]&&formObj.append(key,formData[key]);formObj.append("fire_away","Upload"),overwriteFile&&formObj.append("overwrite",!0),$.ajax({type:"POST",url:loris.BaseURL+"/imaging_uploader/",data:formObj,cache:!1,contentType:!1,processData:!1,xhr:function(){var xhr=new window.XMLHttpRequest;return xhr.upload.addEventListener("progress",function(evt){if(evt.lengthComputable){var percentage=Math.round(evt.loaded/evt.total*100);this.setState({uploadProgress:percentage})}}.bind(this),!1),xhr}.bind(this),success:function(data){swal({title:"Upload Successful!",type:"success"},function(){window.location.assign(loris.BaseURL+"/imaging_uploader/")})},error:function(err){var errMessage="The following errors occured while attempting to display this page:",responseText=err.responseText;responseText.indexOf(errMessage)>-1&&(responseText=responseText.replace("history.back()","location.reload()"),document.open(),document.write(responseText),document.close()),console.error(err)}.bind(this)})}},{key:"render",value:function(){var form=this.state.form;form.IsPhantom.value=this.state.formData.IsPhantom,form.candID.value=this.state.formData.candID,form.pSCID.value=this.state.formData.pSCID,form.visitLabel.value=this.state.formData.visitLabel,form.mri_file.value=this.state.formData.mri_file;var btnClass=this.state.uploadProgress>-1?"btn btn-primary hide":void 0,notes="File name must be of type .tgz or tar.gz or .zip. Uploads cannot exceed "+this.props.maxUploadSize;return React.createElement("div",{className:"row"},React.createElement("div",{className:"col-md-7"},React.createElement("h3",null,"Upload an imaging scan"),React.createElement("br",null),React.createElement(FormElement,{name:"upload_form",formElements:form,fileUpload:!0,onUserInput:this.onFormChange},React.createElement(StaticElement,{label:"Notes",text:notes}),React.createElement("div",{className:"row"},React.createElement("div",{className:"col-sm-9 col-sm-offset-3"},React.createElement(_ProgressBar2.default,{value:this.state.uploadProgress}))),React.createElement(ButtonElement,{onUserInput:this.submitForm,buttonClass:btnClass}))))}}]),UploadForm}(React.Component);UploadForm.propTypes={},UploadForm.defaultProps={},exports.default=UploadForm},function(module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i-1)return null;var row={};rowHeaders.forEach(function(header,index){row[header]=rowData[index]},this);var cellStyle={whiteSpace:"nowrap"};if("Progress"===column){if("Failure"===cell)return cellStyle.color="#fff",React.createElement("td",{className:"label-danger",style:cellStyle},cell);if("In Progress..."===cell)return cellStyle.color="#fff",React.createElement("td",{className:"label-warning",style:cellStyle},cell);var created=row["Number Of MincCreated"],inserted=row["Number Of MincInserted"];return React.createElement("td",{style:cellStyle},cell," (",inserted," out of ",created,")")}if("Tarchive Info"===column){if(!cell||"0"===cell)return React.createElement("td",null);var url=loris.BaseURL+"/dicom_archive/viewDetails/?tarchiveID="+cell;return React.createElement("td",{style:cellStyle},React.createElement("a",{href:url},"View Details"))}if("Number Of MincInserted"===column&&cell>0)return React.createElement("td",{style:cellStyle},React.createElement("a",{onClick:handleClick.bind(null,row.CandID)},cell));if("Number Of MincCreated"===column){var violatedScans=void 0;if(row["Number Of MincCreated"]-row["Number Of MincInserted"]>0){var numViolatedScans=row["Number Of MincCreated"]-row["Number Of MincInserted"],patientName=row.PatientName;violatedScans=React.createElement("a",{onClick:openViolatedScans.bind(null,patientName)},"(",numViolatedScans," violated scans)")}return React.createElement("td",{style:cellStyle},cell," ",violatedScans)}return React.createElement("td",{style:cellStyle},cell)}Object.defineProperty(exports,"__esModule",{value:!0}),loris.hiddenHeaders=["PatientName"],exports.default=formatColumn}]);
+!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}var _ImagingUploader=__webpack_require__(15),_ImagingUploader2=_interopRequireDefault(_ImagingUploader);$(function(){var imagingUploader=React.createElement("div",{className:"page-imaging-uploader"},React.createElement(_ImagingUploader2.default,{Module:"imaging_uploader",DataURL:loris.BaseURL+"/imaging_uploader/?format=json"}));ReactDOM.render(imagingUploader,document.getElementById("lorisworkspace"))})},,function(module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i0&&(activeTab=_this.props.tabs[0].id),_this.state={activeTab:activeTab},_this.handleClick=_this.handleClick.bind(_this),_this.getTabs=_this.getTabs.bind(_this),_this.getTabPanes=_this.getTabPanes.bind(_this),_this}return _inherits(Tabs,_React$Component),_createClass(Tabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("ul",{className:"nav nav-tabs",role:"tablist",style:tabStyle},tabs),React.createElement("div",{className:"tab-content"},tabPanes))}}]),Tabs}(React.Component);Tabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},Tabs.defaultProps={onTabChange:function(){},updateURL:!0};var VerticalTabs=function(_React$Component2){function VerticalTabs(props){_classCallCheck(this,VerticalTabs);var _this2=_possibleConstructorReturn(this,(VerticalTabs.__proto__||Object.getPrototypeOf(VerticalTabs)).call(this,props)),hash=window.location.hash,activeTab="";return _this2.props.updateURL&&hash?activeTab=hash.substr(1):_this2.props.defaultTab?activeTab=_this2.props.defaultTab:_this2.props.tabs.length>0&&(activeTab=_this2.props.tabs[0].id),_this2.state={activeTab:activeTab},_this2.handleClick=_this2.handleClick.bind(_this2),_this2.getTabs=_this2.getTabs.bind(_this2),_this2.getTabPanes=_this2.getTabPanes.bind(_this2),_this2}return _inherits(VerticalTabs,_React$Component2),_createClass(VerticalTabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("div",{className:"tabbable col-md-3 col-sm-3"},React.createElement("ul",{className:"nav nav-pills nav-stacked",role:"tablist",style:tabStyle},tabs)),React.createElement("div",{className:"tab-content col-md-9 col-sm-9"},tabPanes))}}]),VerticalTabs}(React.Component);VerticalTabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},VerticalTabs.defaultProps={onTabChange:function(){},updateURL:!0};var TabPane=function(_React$Component3){function TabPane(){return _classCallCheck(this,TabPane),_possibleConstructorReturn(this,(TabPane.__proto__||Object.getPrototypeOf(TabPane)).apply(this,arguments))}return _inherits(TabPane,_React$Component3),_createClass(TabPane,[{key:"render",value:function(){var classList="tab-pane",title=void 0;return this.props.TabId===this.props.activeTab&&(classList+=" active"),this.props.Title&&(title=React.createElement("h1",null,this.props.Title)),React.createElement("div",{role:"tabpanel",className:classList,id:this.props.TabId},title,this.props.children)}}]),TabPane}(React.Component);TabPane.propTypes={TabId:React.PropTypes.string.isRequired,Title:React.PropTypes.string,activeTab:React.PropTypes.string},exports.Tabs=Tabs,exports.VerticalTabs=VerticalTabs,exports.TabPane=TabPane},,,,function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(obj){return typeof obj}:function(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_createClass=function(){function defineProperties(target,props){for(var i=0;i",logType:"summary"},_this.initHelper=_this.initHelper.bind(_this),_this.onLogTypeChange=_this.onLogTypeChange.bind(_this),_this.setServerPolling=_this.setServerPolling.bind(_this),_this.monitorProgress=_this.monitorProgress.bind(_this),_this}return _inherits(LogPanel,_React$Component),_createClass(LogPanel,[{key:"componentDidMount",value:function(){this.initHelper()}},{key:"initHelper",value:function(){var uploadProgress=new UploadProgress;this.uploadProgress=uploadProgress,$("#mri_upload_table").on("click","tbody tr",function(event){return null!==uploadProgress.getUploadRow()&&($(uploadProgress.getUploadRow()).css("background-color","white"),this.setServerPolling(!1)),event.currentTarget===uploadProgress.getUploadRow()?(uploadProgress.setUploadRow(null),uploadProgress.setProgressFromServer(null),void this.setState({logText:""})):(uploadProgress.setUploadRow(event.currentTarget),$(event.currentTarget).css("background-color","#EFEFFB"),void this.monitorProgress(this.state.logType))}.bind(this))}},{key:"monitorProgress",value:function(logType){var summary="summary"===logType,uploadProgress=this.uploadProgress,uploadId=uploadProgress.getUploadId();uploadId&&$.post(loris.BaseURL+"/imaging_uploader/ajax/getUploadSummary.php",{uploadId:uploadId,summary:summary},function(data){uploadProgress.setProgressFromServer(data),this.setState({logText:uploadProgress.getProgressText()}),this.setServerPolling(uploadProgress.getPipelineStatus()===UploadProgress.PIPELINE_STATUS_RUNNING)}.bind(this))}},{key:"setServerPolling",value:function(poll){var uploadProgress=this.uploadProgress;poll?(this.setServerPolling.getSummaryInterval||(this.setServerPolling.getSummaryInterval=setInterval(this.monitorProgress,5e3)),this.setServerPolling.dotUpdateInterval||(this.setServerPolling.dotUpdateInterval=setInterval(function(){uploadProgress.updateDots(),this.setState({logText:uploadProgress.getProgressText()})},3e3)),this.setServerPolling.animatedCharInterval||(this.setServerPolling.animatedCharInterval=setInterval(function(){uploadProgress.updateAnimatedCharIndex(),this.setState({logText:uploadProgress.getProgressText()})},250))):(this.setServerPolling.getSummaryInterval&&(clearInterval(this.setServerPolling.getSummaryInterval),this.setServerPolling.getSummaryInterval=null),this.setServerPolling.dotUpdateInterval&&(clearInterval(this.setServerPolling.dotUpdateInterval),this.setServerPolling.dotUpdateInterval=null),this.setServerPolling.animatedCharInterval&&(clearInterval(this.setServerPolling.animatedCharInterval),this.setServerPolling.animatedCharInterval=null))}},{key:"onLogTypeChange",value:function(name,value){this.monitorProgress(value),this.setState({logType:value})}},{key:"render",value:function(){var logTypes={summary:"Summary",detailed:"Detailed"};return React.createElement(_Panel2.default,{id:"log_panel",title:"Log Viewer"},React.createElement(FormElement,{name:"log_form"},React.createElement(SelectElement,{name:"LogType",label:"Logs to display",options:logTypes,onUserInput:this.onLogTypeChange,value:this.state.logType,emptyOption:!1}),React.createElement(TextareaElement,{name:"UploadLogs",disabled:!0,id:"mri_upload_logs",value:this.state.logText,rows:6})))}}]),LogPanel}(React.Component);LogPanel.propTypes={},LogPanel.defaultProps={},exports.default=LogPanel},function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i-1});if(!mriFile)return void this.uploadFile();if("Success"===mriFile.status)return void swal({title:"File already exists!",text:"A file with this name has already successfully passed the MRI pipeline!\n",type:"error",confirmButtonText:"OK"});if("In Progress..."===mriFile.status)return void swal({title:"File is currently processing!",text:"A file with this name is currently going through the MRI pipeline!\n",type:"error",confirmButtonText:"OK"});"Failure"===mriFile.status&&swal({title:"Are you sure?",text:"A file with this name already exists!\n Would you like to override existing file?",type:"warning",showCancelButton:!0,confirmButtonText:"Yes, I am sure!",cancelButtonText:"No, cancel it!"},function(isConfirm){isConfirm?this.uploadFile(!0):swal("Cancelled","Your imaginary file is safe :)","error")}.bind(this)),"Not Started"===mriFile.status&&swal({title:"Are you sure?",text:"A file with this name has been uploaded but has not yet started the MRI pipeline.\n Would you like to override the existing file?",type:"warning",showCancelButton:!0,confirmButtonText:"Yes, I am sure!",cancelButtonText:"No, cancel it!"},function(isConfirm){isConfirm?this.uploadFile(!0):swal("Cancelled","Your upload has been cancelled.","error")}.bind(this))}}},{key:"uploadFile",value:function(overwriteFile){var formData=this.state.formData,formObj=new FormData;for(var key in formData)""!==formData[key]&&formObj.append(key,formData[key]);formObj.append("fire_away","Upload"),overwriteFile&&formObj.append("overwrite",!0),$.ajax({type:"POST",url:loris.BaseURL+"/imaging_uploader/",data:formObj,cache:!1,contentType:!1,processData:!1,xhr:function(){var xhr=new window.XMLHttpRequest;return xhr.upload.addEventListener("progress",function(evt){if(evt.lengthComputable){var percentage=Math.round(evt.loaded/evt.total*100);this.setState({uploadProgress:percentage})}}.bind(this),!1),xhr}.bind(this),success:function(data){swal({title:"Upload Successful!",type:"success"},function(){window.location.assign(loris.BaseURL+"/imaging_uploader/")})},error:function(err){var errMessage="The following errors occured while attempting to display this page:",responseText=err.responseText;responseText.indexOf(errMessage)>-1&&(responseText=responseText.replace("history.back()","location.reload()"),document.open(),document.write(responseText),document.close()),console.error(err)}})}},{key:"render",value:function(){var form=this.state.form;form.IsPhantom.value=this.state.formData.IsPhantom,form.candID.value=this.state.formData.candID,form.pSCID.value=this.state.formData.pSCID,form.visitLabel.value=this.state.formData.visitLabel,form.mri_file.value=this.state.formData.mri_file;var btnClass=this.state.uploadProgress>-1?"btn btn-primary hide":void 0,notes="File name must be of type .tgz or tar.gz or .zip. Uploads cannot exceed "+this.props.maxUploadSize;return React.createElement("div",{className:"row"},React.createElement("div",{className:"col-md-7"},React.createElement("h3",null,"Upload an imaging scan"),React.createElement("br",null),React.createElement(FormElement,{name:"upload_form",formElements:form,fileUpload:!0,onUserInput:this.onFormChange},React.createElement(StaticElement,{label:"Notes",text:notes}),React.createElement("div",{className:"row"},React.createElement("div",{className:"col-sm-9 col-sm-offset-3"},React.createElement(_ProgressBar2.default,{value:this.state.uploadProgress}))),React.createElement(ButtonElement,{onUserInput:this.submitForm,buttonClass:btnClass}))))}}]),UploadForm}(React.Component);UploadForm.propTypes={},UploadForm.defaultProps={},exports.default=UploadForm},function(module,exports){
+"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i-1)return null;var row={};rowHeaders.forEach(function(header,index){row[header]=rowData[index]},this);var cellStyle={whiteSpace:"nowrap"};if("Progress"===column){if("Failure"===cell)return cellStyle.color="#fff",React.createElement("td",{className:"label-danger",style:cellStyle},cell);if("In Progress..."===cell)return cellStyle.color="#fff",React.createElement("td",{className:"label-warning",style:cellStyle},cell);var created=row["Number Of MincCreated"],inserted=row["Number Of MincInserted"];return React.createElement("td",{style:cellStyle},cell," (",inserted," out of ",created,")")}if("Tarchive Info"===column){if(!cell||"0"===cell)return React.createElement("td",null);var url=loris.BaseURL+"/dicom_archive/viewDetails/?tarchiveID="+cell;return React.createElement("td",{style:cellStyle},React.createElement("a",{href:url},"View Details"))}if("Number Of MincInserted"===column&&cell>0)return React.createElement("td",{style:cellStyle},React.createElement("a",{onClick:handleClick.bind(null,row.CandID)},cell));if("Number Of MincCreated"===column){var violatedScans=void 0;if(row["Number Of MincCreated"]-row["Number Of MincInserted"]>0){var numViolatedScans=row["Number Of MincCreated"]-row["Number Of MincInserted"],patientName=row.PatientName;violatedScans=React.createElement("a",{onClick:openViolatedScans.bind(null,patientName)},"(",numViolatedScans," violated scans)")}return React.createElement("td",{style:cellStyle},cell," ",violatedScans)}return React.createElement("td",{style:cellStyle},cell)}Object.defineProperty(exports,"__esModule",{value:!0}),loris.hiddenHeaders=["PatientName"],exports.default=formatColumn}]);
//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/modules/imaging_uploader/php/imaging_uploader.class.inc b/modules/imaging_uploader/php/imaging_uploader.class.inc
index 2dc3822c6a2..508508ee52c 100644
--- a/modules/imaging_uploader/php/imaging_uploader.class.inc
+++ b/modules/imaging_uploader/php/imaging_uploader.class.inc
@@ -33,6 +33,8 @@ class Imaging_Uploader extends \NDB_Menu_Filter_Form
/**
* ID of the uploaded file in mri_upload table (if upload successful).
+ * If the file is a reupload of an existing file, then it will be set to
+ * the ID of the original upload.
*/
var $mri_upload_id;
@@ -57,7 +59,7 @@ class Imaging_Uploader extends \NDB_Menu_Filter_Form
/**
* Sets up the Filter Variables
*
- * @return boolean
+ * @return void
*/
function _setupVariables()
{
@@ -106,7 +108,7 @@ class Imaging_Uploader extends \NDB_Menu_Filter_Form
/**
* Sets up the smarty menu filter items for the imaging uploader
*
- * @return none
+ * @return void
*/
function setup()
{
@@ -294,7 +296,7 @@ class Imaging_Uploader extends \NDB_Menu_Filter_Form
///////////////////////////////////////////////////////////////////////
$pcv = $pscid . "_" . $candid . "_" . $visit_label;
$pcvu = $pcv . "_";
- if ((!preg_match("/{$pcv}\.(zip|tgz|tar.gz)/", $file_name))
+ if ((!preg_match("/^{$pcv}\.(zip|tgz|tar.gz)/", $file_name))
&& (!preg_match("/^{$pcvu}.*(\.(zip|tgz|tar.gz))/", $file_name))
) {
$errors[] = "File name must match " . $pcv .
@@ -453,6 +455,8 @@ class Imaging_Uploader extends \NDB_Menu_Filter_Form
);
if (!empty($id)) {
$db->update('mri_upload', $values, array('UploadID' => $id));
+ $this->mri_upload_id = $id;
+
return true;
}
}
@@ -658,7 +662,7 @@ class Imaging_Uploader extends \NDB_Menu_Filter_Form
* Converts the results of this menu filter to a JSON format to be retrieved
* with ?format=json
*
- * @return a json encoded string of the headers and data from this table
+ * @return string a json encoded string of the headers and data from this table
*/
function toJSON()
{
diff --git a/modules/imaging_uploader/test/TestPlan.md b/modules/imaging_uploader/test/TestPlan.md
index db50af9eca2..da44facd4cd 100644
--- a/modules/imaging_uploader/test/TestPlan.md
+++ b/modules/imaging_uploader/test/TestPlan.md
@@ -66,11 +66,18 @@
(PSCID and visit label should be correct though). Check that an appropriate message is written in the Console
Output.
[Manual Testing]
-18. Upload a valid, anonymized .tar.gz DICOM archive but with a PSCID that does not match the one in the archive
+18. First, set the config setting 'ImagingUploader auto launch' to 'Yes'. Then, edit your prod file (in
+ /dicom-archive/.loris-mri) and comment out the definition of the @db array. Once these operations
+ are done, upload any valid scan: check in the server processes manager that this fails with an error code of 4.
+ Now try to upload the scan again. When the system asks you if you want to overwrite the existing
+ archive, answer 'Yes'. Check that this reupload fails with error code 4 (and not 2).
+ Related to Redmine#14093 and PR#3555.
+ [Manual Testing]
+19. Upload a valid, anonymized .tar.gz DICOM archive but with a PSCID that does not match the one in the archive
(CandID and visit label should be correct though). Check that an appropriate message is written in the Console
Output.
[Automation Testing]
-19. Upload a valid, anonymized .tar.gz DICOM archive but with a visit label that does not match the one in the
+20. Upload a valid, anonymized .tar.gz DICOM archive but with a visit label that does not match the one in the
archive (CandID and PSCID should be correct though). Check that an appropriate message is written in the Console
Output.
[Automation Testing]
diff --git a/modules/instrument_builder/README.md b/modules/instrument_builder/README.md
new file mode 100644
index 00000000000..36fe648dd14
--- /dev/null
+++ b/modules/instrument_builder/README.md
@@ -0,0 +1,31 @@
+# Instrument Builder
+
+## Purpose
+
+The instrument builder provides a graphical interface for users to
+design and build instruments to be installed on a LORIS instance.
+
+## Intended Users
+
+The instrument builder is intended to be used by non-programmers who
+need to construct instruments to install in LORIS.
+
+## Scope
+
+The instrument builder module only builds the instrument and saves them
+to the user's local filesystem. It does not install the instrument
+(see: the `instrument_manager` module for that) or provide data entry
+capabilities.
+
+## Permissions
+
+The `instrument_builder` permission is required to access the module.
+
+## Configurations
+
+None.
+
+## Interactions with LORIS
+
+None. (The instrument builder is entirely written in client-side
+javascript.)
diff --git a/modules/instrument_builder/js/react.instrument_builder.js b/modules/instrument_builder/js/react.instrument_builder.js
index ef1e44b2ea5..a56440344b0 100644
--- a/modules/instrument_builder/js/react.instrument_builder.js
+++ b/modules/instrument_builder/js/react.instrument_builder.js
@@ -1,2 +1,2 @@
-!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}({0:function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(target){for(var i=1;i=height?(this.nodePlacement="after",parent.insertBefore(this.getPlaceholder(),targetRow.nextElementSibling)):(this.nodePlacement="before",parent.insertBefore(this.getPlaceholder(),targetRow))}},render:function(){var tableRows=this.props.elements.map(function(element,i){var row=void 0,colStyles={wordWrap:"break-word"};return row=element.editing?React.createElement("tr",{"data-id":i,key:i,draggable:this.props.draggable,onDragEnd:this.dragEnd,onDragStart:this.dragStart},React.createElement("td",{className:"col-xs-2",colSpan:"3"},React.createElement(AddElement,{updateQuestions:this.props.updateElement,element:element,index:i}))):React.createElement("tr",{"data-id":i,key:i,draggable:this.props.draggable,onDragEnd:this.dragEnd,onDragStart:this.dragStart},React.createElement("td",{style:colStyles},element.Name),React.createElement("td",{style:colStyles},React.createElement(LorisElement,{element:element})),React.createElement("td",{style:colStyles},React.createElement("button",{onClick:this.props.editElement.bind(null,i),className:"button"},"Edit"),React.createElement("button",{onClick:this.props.deleteElement.bind(null,i),className:"button"},"Delete")))}.bind(this)),tableStyles={tableLayout:"fixed"};return React.createElement("table",{id:"sortable",className:"table table-hover",style:tableStyles},React.createElement("thead",null,React.createElement("tr",null,React.createElement("th",{className:"col-xs-2"},"Database Name"),React.createElement("th",{className:"col-xs-6"},"Question Display (Front End)"),React.createElement("th",{className:"col-xs-4"},"Edit"))),React.createElement("tbody",{onDragOver:this.dragOver},tableRows))}}),BuildPane=React.createClass({displayName:"BuildPane",getInitialState:function(){return{Elements:[{Type:"ElementGroup",GroupType:"Page",Description:"Top",Elements:[]}],amountEditing:0,currentPage:0,elementDBNames:{}}},loadElements:function(elements){var elContent=elements[this.state.currentPage].Elements,elNames={};elContent.forEach(function(el){elNames[el.Name]=""}),this.setState({Elements:elements,elementDBNames:elNames})},editElement:function(elementIndex){this.setState(function(state){var temp=state.Elements,edit=state.amountEditing+1,dbNames=state.elementDBNames;return delete dbNames[temp[state.currentPage].Elements[elementIndex].Name],temp[state.currentPage].Elements[elementIndex].editing=!0,{Elements:temp,amountEditing:edit,elementDBNames:dbNames}})},deleteElement:function(elementIndex){this.setState(function(state){var temp=state.Elements,dbNames=state.elementDBNames;return delete dbNames[temp[state.currentPage].Elements[elementIndex].Name],temp[state.currentPage].Elements.splice(elementIndex,1),{Elements:temp}})},updateElement:function(element,index){return!(element.Name&&element.Name in this.state.elementDBNames)&&(this.setState(function(state){var temp=state.Elements,edit=state.amountEditing-1,dbNa=state.elementDBNames;return temp[state.currentPage].Elements[index]=element,element.Name&&(dbNa[element.Name]=""),{Elements:temp,amountEditing:edit,elementDBNames:dbNa}}),!0)},addQuestion:function(element){return!(element.Name&&element.Name in this.state.elementDBNames)&&(this.setState(function(state){var temp=state.Elements,dbNa=state.elementDBNames;return element.Name&&(dbNa[element.Name]=""),temp[state.currentPage].Elements.push(element),{Elements:temp,elementDBNames:dbNa}}),!0)},addPage:function(pageName){this.setState(function(state){var temp=state.Elements,page=state.currentPage+1;return temp.push({Type:"ElementGroup",GroupType:"Page",Description:pageName,Elements:[]}),{Elements:temp,currentPage:page}})},selectPage:function(index){this.setState({currentPage:index})},render:function(){var draggable=0===this.state.amountEditing,pages=this.state.Elements.map(function(element,i){return React.createElement("li",{key:i,onClick:this.selectPage.bind(null,i)},React.createElement("a",null,this.state.Elements[i].Description))}.bind(this));return React.createElement(_Tabs.TabPane,_extends({Title:"Build Instrument"},this.props),React.createElement("div",{className:"form-group col-xs-12"},React.createElement("label",{htmlFor:"selected-input",className:"col-xs-2 col-sm-1 control-label"},"Page:"),React.createElement("div",{className:"col-sm-4"},React.createElement("div",{className:"btn-group"},React.createElement("button",{id:"selected-input",type:"button",className:"btn btn-default dropdown-toggle","data-toggle":"dropdown"},React.createElement("span",{id:"search_concept"},this.state.Elements[this.state.currentPage].Description),React.createElement("span",{className:"caret"})),React.createElement("ul",{className:"dropdown-menu",role:"menu"},pages)))),React.createElement(DisplayElements,{elements:this.state.Elements[this.state.currentPage].Elements,editElement:this.editElement,deleteElement:this.deleteElement,updateElement:this.updateElement,draggable:draggable}),React.createElement("div",{className:"row"},React.createElement(AddElement,{updateQuestions:this.addQuestion,addPage:this.addPage})))}}),InstrumentBuilderApp=React.createClass({displayName:"InstrumentBuilderApp",saveInstrument:function(){Instrument.save(this.refs.savePane.state,this.refs.buildPane.state.Elements)},loadCallback:function(elements,info){this.refs.savePane.loadState(info),this.refs.buildPane.loadElements(elements),this.refs.loadPane.setAlert("success")},render:function(){var tabs=[];tabs.push(React.createElement(LoadPane,{TabId:"Load",ref:"loadPane",loadCallback:this.loadCallback,key:1})),tabs.push(React.createElement(BuildPane,{TabId:"Build",ref:"buildPane",key:2})),tabs.push(React.createElement(SavePane,{TabId:"Save",ref:"savePane",save:this.saveInstrument,key:3}));var tabList=[{id:"Load",label:"Load"},{id:"Build",label:"Build"},{id:"Save",label:"Save"}];return React.createElement("div",null,React.createElement(_Tabs.Tabs,{tabs:tabList,defaultTab:"Build"},tabs))}}),RInstrumentBuilderApp=React.createFactory(InstrumentBuilderApp);window.RInstrumentBuilderApp=RInstrumentBuilderApp,exports.default=InstrumentBuilderApp},9:function(module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i0&&(activeTab=_this.props.tabs[0].id),_this.state={activeTab:activeTab},_this.handleClick=_this.handleClick.bind(_this),_this.getTabs=_this.getTabs.bind(_this),_this.getTabPanes=_this.getTabPanes.bind(_this),_this}return _inherits(Tabs,_React$Component),_createClass(Tabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("ul",{className:"nav nav-tabs",role:"tablist",style:tabStyle},tabs),React.createElement("div",{className:"tab-content"},tabPanes))}}]),Tabs}(React.Component);Tabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},Tabs.defaultProps={onTabChange:function(){},updateURL:!1};var TabPane=function(_React$Component2){function TabPane(){return _classCallCheck(this,TabPane),_possibleConstructorReturn(this,(TabPane.__proto__||Object.getPrototypeOf(TabPane)).apply(this,arguments))}return _inherits(TabPane,_React$Component2),_createClass(TabPane,[{key:"render",value:function(){var classList="tab-pane",title=void 0;return this.props.TabId===this.props.activeTab&&(classList+=" active"),this.props.Title&&(title=React.createElement("h1",null,this.props.Title)),React.createElement("div",{role:"tabpanel",className:classList,id:this.props.TabId},title,this.props.children)}}]),TabPane}(React.Component);TabPane.propTypes={TabId:React.PropTypes.string.isRequired,Title:React.PropTypes.string,activeTab:React.PropTypes.string},exports.Tabs=Tabs,exports.TabPane=TabPane}});
+!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}({0:function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(target){for(var i=1;i=height?(this.nodePlacement="after",parent.insertBefore(this.getPlaceholder(),targetRow.nextElementSibling)):(this.nodePlacement="before",parent.insertBefore(this.getPlaceholder(),targetRow))}},render:function(){var tableRows=this.props.elements.map(function(element,i){var row=void 0,colStyles={wordWrap:"break-word"};return row=element.editing?React.createElement("tr",{"data-id":i,key:i,draggable:this.props.draggable,onDragEnd:this.dragEnd,onDragStart:this.dragStart},React.createElement("td",{className:"col-xs-2",colSpan:"3"},React.createElement(AddElement,{updateQuestions:this.props.updateElement,element:element,index:i}))):React.createElement("tr",{"data-id":i,key:i,draggable:this.props.draggable,onDragEnd:this.dragEnd,onDragStart:this.dragStart},React.createElement("td",{style:colStyles},element.Name),React.createElement("td",{style:colStyles},React.createElement(LorisElement,{element:element})),React.createElement("td",{style:colStyles},React.createElement("button",{onClick:this.props.editElement.bind(null,i),className:"button"},"Edit"),React.createElement("button",{onClick:this.props.deleteElement.bind(null,i),className:"button"},"Delete")))}.bind(this)),tableStyles={tableLayout:"fixed"};return React.createElement("table",{id:"sortable",className:"table table-hover",style:tableStyles},React.createElement("thead",null,React.createElement("tr",null,React.createElement("th",{className:"col-xs-2"},"Database Name"),React.createElement("th",{className:"col-xs-6"},"Question Display (Front End)"),React.createElement("th",{className:"col-xs-4"},"Edit"))),React.createElement("tbody",{onDragOver:this.dragOver},tableRows))}}),BuildPane=React.createClass({displayName:"BuildPane",getInitialState:function(){return{Elements:[{Type:"ElementGroup",GroupType:"Page",Description:"Top",Elements:[]}],amountEditing:0,currentPage:0,elementDBNames:{}}},loadElements:function(elements){var elContent=elements[this.state.currentPage].Elements,elNames={};elContent.forEach(function(el){elNames[el.Name]=""}),this.setState({Elements:elements,elementDBNames:elNames})},editElement:function(elementIndex){this.setState(function(state){var temp=state.Elements,edit=state.amountEditing+1,dbNames=state.elementDBNames;return delete dbNames[temp[state.currentPage].Elements[elementIndex].Name],temp[state.currentPage].Elements[elementIndex].editing=!0,{Elements:temp,amountEditing:edit,elementDBNames:dbNames}})},deleteElement:function(elementIndex){this.setState(function(state){var temp=state.Elements,dbNames=state.elementDBNames;return delete dbNames[temp[state.currentPage].Elements[elementIndex].Name],temp[state.currentPage].Elements.splice(elementIndex,1),{Elements:temp}})},updateElement:function(element,index){return!(element.Name&&element.Name in this.state.elementDBNames)&&(this.setState(function(state){var temp=state.Elements,edit=state.amountEditing-1,dbNa=state.elementDBNames;return temp[state.currentPage].Elements[index]=element,element.Name&&(dbNa[element.Name]=""),{Elements:temp,amountEditing:edit,elementDBNames:dbNa}}),!0)},addQuestion:function(element){return!(element.Name&&element.Name in this.state.elementDBNames)&&(this.setState(function(state){var temp=state.Elements,dbNa=state.elementDBNames;return element.Name&&(dbNa[element.Name]=""),temp[state.currentPage].Elements.push(element),{Elements:temp,elementDBNames:dbNa}}),!0)},addPage:function(pageName){this.setState(function(state){var temp=state.Elements,page=state.currentPage+1;return temp.push({Type:"ElementGroup",GroupType:"Page",Description:pageName,Elements:[]}),{Elements:temp,currentPage:page}})},selectPage:function(index){this.setState({currentPage:index})},render:function(){var draggable=0===this.state.amountEditing,pages=this.state.Elements.map(function(element,i){return React.createElement("li",{key:i,onClick:this.selectPage.bind(null,i)},React.createElement("a",null,this.state.Elements[i].Description))}.bind(this));return React.createElement(_Tabs.TabPane,_extends({Title:"Build Instrument"},this.props),React.createElement("div",{className:"form-group col-xs-12"},React.createElement("label",{htmlFor:"selected-input",className:"col-xs-2 col-sm-1 control-label"},"Page:"),React.createElement("div",{className:"col-sm-4"},React.createElement("div",{className:"btn-group"},React.createElement("button",{id:"selected-input",type:"button",className:"btn btn-default dropdown-toggle","data-toggle":"dropdown"},React.createElement("span",{id:"search_concept"},this.state.Elements[this.state.currentPage].Description),React.createElement("span",{className:"caret"})),React.createElement("ul",{className:"dropdown-menu",role:"menu"},pages)))),React.createElement(DisplayElements,{elements:this.state.Elements[this.state.currentPage].Elements,editElement:this.editElement,deleteElement:this.deleteElement,updateElement:this.updateElement,draggable:draggable}),React.createElement("div",{className:"row"},React.createElement(AddElement,{updateQuestions:this.addQuestion,addPage:this.addPage})))}}),InstrumentBuilderApp=React.createClass({displayName:"InstrumentBuilderApp",saveInstrument:function(){Instrument.save(this.refs.savePane.state,this.refs.buildPane.state.Elements)},loadCallback:function(elements,info){this.refs.savePane.loadState(info),this.refs.buildPane.loadElements(elements),this.refs.loadPane.setAlert("success")},render:function(){var tabs=[];tabs.push(React.createElement(LoadPane,{TabId:"Load",ref:"loadPane",loadCallback:this.loadCallback,key:1})),tabs.push(React.createElement(BuildPane,{TabId:"Build",ref:"buildPane",key:2})),tabs.push(React.createElement(SavePane,{TabId:"Save",ref:"savePane",save:this.saveInstrument,key:3}));var tabList=[{id:"Load",label:"Load"},{id:"Build",label:"Build"},{id:"Save",label:"Save"}];return React.createElement("div",null,React.createElement(_Tabs.Tabs,{tabs:tabList,defaultTab:"Build"},tabs))}}),RInstrumentBuilderApp=React.createFactory(InstrumentBuilderApp);window.RInstrumentBuilderApp=RInstrumentBuilderApp,exports.default=InstrumentBuilderApp},9:function(module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i0&&(activeTab=_this.props.tabs[0].id),_this.state={activeTab:activeTab},_this.handleClick=_this.handleClick.bind(_this),_this.getTabs=_this.getTabs.bind(_this),_this.getTabPanes=_this.getTabPanes.bind(_this),_this}return _inherits(Tabs,_React$Component),_createClass(Tabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("ul",{className:"nav nav-tabs",role:"tablist",style:tabStyle},tabs),React.createElement("div",{className:"tab-content"},tabPanes))}}]),Tabs}(React.Component);Tabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},Tabs.defaultProps={onTabChange:function(){},updateURL:!0};var VerticalTabs=function(_React$Component2){function VerticalTabs(props){_classCallCheck(this,VerticalTabs);var _this2=_possibleConstructorReturn(this,(VerticalTabs.__proto__||Object.getPrototypeOf(VerticalTabs)).call(this,props)),hash=window.location.hash,activeTab="";return _this2.props.updateURL&&hash?activeTab=hash.substr(1):_this2.props.defaultTab?activeTab=_this2.props.defaultTab:_this2.props.tabs.length>0&&(activeTab=_this2.props.tabs[0].id),_this2.state={activeTab:activeTab},_this2.handleClick=_this2.handleClick.bind(_this2),_this2.getTabs=_this2.getTabs.bind(_this2),_this2.getTabPanes=_this2.getTabPanes.bind(_this2),_this2}return _inherits(VerticalTabs,_React$Component2),_createClass(VerticalTabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("div",{className:"tabbable col-md-3 col-sm-3"},React.createElement("ul",{className:"nav nav-pills nav-stacked",role:"tablist",style:tabStyle},tabs)),React.createElement("div",{className:"tab-content col-md-9 col-sm-9"},tabPanes))}}]),VerticalTabs}(React.Component);VerticalTabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},VerticalTabs.defaultProps={onTabChange:function(){},updateURL:!0};var TabPane=function(_React$Component3){function TabPane(){return _classCallCheck(this,TabPane),_possibleConstructorReturn(this,(TabPane.__proto__||Object.getPrototypeOf(TabPane)).apply(this,arguments))}return _inherits(TabPane,_React$Component3),_createClass(TabPane,[{key:"render",value:function(){var classList="tab-pane",title=void 0;return this.props.TabId===this.props.activeTab&&(classList+=" active"),this.props.Title&&(title=React.createElement("h1",null,this.props.Title)),React.createElement("div",{role:"tabpanel",className:classList,id:this.props.TabId},title,this.props.children)}}]),TabPane}(React.Component);TabPane.propTypes={TabId:React.PropTypes.string.isRequired,Title:React.PropTypes.string,activeTab:React.PropTypes.string},exports.Tabs=Tabs,exports.VerticalTabs=VerticalTabs,exports.TabPane=TabPane}});
//# sourceMappingURL=react.instrument_builder.js.map
\ No newline at end of file
diff --git a/modules/instrument_builder/php/instrument_builder.class.inc b/modules/instrument_builder/php/instrument_builder.class.inc
index cb6b9a6fc60..a616d5d4b78 100644
--- a/modules/instrument_builder/php/instrument_builder.class.inc
+++ b/modules/instrument_builder/php/instrument_builder.class.inc
@@ -35,7 +35,7 @@ class Instrument_Builder extends \NDB_Form
* Tests that, when loading the Instrument builder module, some
* text appears in the body.
*
- * @return void
+ * @return bool
*/
function _hasAccess()
{
diff --git a/modules/instrument_list/README.md b/modules/instrument_list/README.md
new file mode 100644
index 00000000000..826096a2d42
--- /dev/null
+++ b/modules/instrument_list/README.md
@@ -0,0 +1,63 @@
+# Instrument List
+
+## Purpose
+
+The instrument list module shows an overview of all instruments
+which are part of a given session for a candidate. It provides a
+mechanism for setting session level behavioural QC flags and the
+ability to send a visit to DCC once the instruments are all complete.
+
+## Intended Users
+
+The page is intended to be used by data entry staff to access
+instruments for data entry. It is the primary entry point into a
+visit.
+
+## Scope
+
+The instrument list module provides a path to access the instruments,
+and a control panel which allows session level metadata to be
+entered.
+
+## Permissions
+
+One of three conditions must be met to have access to the module:
+
+1. The user has the permission `access_all_profiles`
+2. The user is at the same site as the visit.
+3. The user is at the same site as one of the candidate's other visits.
+
+The `send_to_dcc` permission is required in order to send the
+timepoint to DCC (finalize the visit).
+
+The separate (and usually more restricted) `unsend_to_dcc` permission
+is required to reverse the `send_to_dcc` in case it was sent in error
+(but should rarely be necessary, as 'Sent to DCC' implies that the data
+is complete and ready to be used for publication, after which it should
+be immutable.)
+
+## Configurations
+
+The instrument list module gets the list of instruments from the
+`flag` table. The test battery must separately be set up (and the
+stage started) in order for any instruments to appear.
+
+The `test_subgroups` SQL table provides a mechanism for grouping
+the instruments on the page into separate sections of the table.
+
+The `test_names` SQL table provides a mapping of short `test_name`
+to a friendly human readable display format which is shown in the
+list.
+
+## Interactions with LORIS
+
+The module primarily links to instruments, which are coded separately.
+It also includes a link to the `next_stage` module in the control
+panel, in order to allow data entry staff to start a visit.
+
+Survey module instruments are inserted into the `flag` table and thus
+show up in the `instrument_list` module, but this is not the case until
+after the survey has been sent.
+
+The `bvl_feedback` thread status for instruments are displayed in
+the list of instruments.
diff --git a/modules/instrument_list/php/instrument_list_controlpanel.class.inc b/modules/instrument_list/php/instrument_list_controlpanel.class.inc
index 6be563cd77f..9e9f289872d 100644
--- a/modules/instrument_list/php/instrument_list_controlpanel.class.inc
+++ b/modules/instrument_list/php/instrument_list_controlpanel.class.inc
@@ -280,29 +280,21 @@ class Instrument_List_ControlPanel extends \TimePoint
// set the BVLQC flag
$user =& \User::singleton();
- if (isset($_REQUEST['setBVLQCStatus'])
+ if (!empty($_REQUEST['setBVLQCStatus'])
&& $user->hasPermission('bvl_feedback')
) {
- $status = \Utility::nullifyEmpty(
- $_REQUEST['setBVLQCStatus'],
- 'BVLQCStatus'
- );
- $this->setData('BVLQCStatus', $status);
+ $this->setData('BVLQCStatus', $_REQUEST['setBVLQCStatus']);
return;
- }
- if (isset($_REQUEST['setBVLQCType']) && !empty($_REQUEST['setBVLQCType'])
- && $user->hasPermission('bvl_feedback')
- ) {
- $QCtype = \Utility::nullifyEmpty(
- $_REQUEST['setBVLQCType'],
- 'BVLQCType'
- );
- $this->setData('BVLQCType', $QCtype);
+ } else {
+ $this->setData('BVLQCStatus', null);
return;
}
- if (empty($_REQUEST['setBVLQCType'])
+ if (!empty($_REQUEST['setBVLQCType'])
&& $user->hasPermission('bvl_feedback')
) {
+ $this->setData('BVLQCType', $_REQUEST['setBVLQCType']);
+ return;
+ } else {
$this->setData('BVLQCType', null);
return;
}
diff --git a/modules/instrument_manager/README.md b/modules/instrument_manager/README.md
new file mode 100644
index 00000000000..a8777e45488
--- /dev/null
+++ b/modules/instrument_manager/README.md
@@ -0,0 +1,52 @@
+# Instrument Manager
+
+## Purpose
+
+The instrument manager is intended to view and manage instrument
+metadata for a LORIS instance as well as provide a centralized place
+to install instruments created from the instrument builder without
+backend access.
+
+It provides overview of the "health" of the various MySQL instrument
+tables and whether or not they are in sync with the instrument as it
+exists on the filesystem or not. However, the checks are only possible
+for LINST (instrument builder) instruments, as the appropriate state
+of database tables can not easily be statically determined for PHP
+instruments.
+
+## Intended Users
+
+The instrument manager is intended to be used by study or LORIS
+administrators to view and edit the instruments installed on a given
+LORIS instance.
+
+## Scope
+
+Only the instrument metadata is managed by the instrument manager.
+
+It does not concern itself with data collection or analysis,
+which are the responsibility of the instruments themselves, nor does
+it concern itself with the creation of instruments which is the
+responsibility of the `instrument_builder` module or PHP programmer.
+The test battery is also managed seperately directly from the database.
+
+## Permissions
+
+The `instrument_manager` module requires the LORIS `superuser`
+permission to be accessed.
+
+## Configurations
+
+For basic access to the module, no configuration is required.
+
+In order to enable the ability to upload instruments, PHP must be
+able to write to the `project/instruments` and `project/tables_sql`
+directories (to write the instrument itself, and instrument table
+patch respectively.)
+
+In order to automatically source the SQL patch and fully configure
+LINST instruments, the LORIS `quatUser` and `quatPassword` configuration
+must be set to a user which has the MySQL `CREATE TABLE` permission.
+(The name `quatUser` and `quatPassword` is an anachronism and should
+be renamed.)
+
diff --git a/modules/instrument_manager/help/instrument_manager.md b/modules/instrument_manager/help/instrument_manager.md
index e544c5babdb..b827a0c40df 100644
--- a/modules/instrument_manager/help/instrument_manager.md
+++ b/modules/instrument_manager/help/instrument_manager.md
@@ -1,6 +1,54 @@
# Instrument manager
-The Instrument Manager module allows LORIS Admin-level superusers to upload and install instrument forms (*.linst files), and to monitor instrument status in LORIS.
+## Uploading Instruments
-To upload an instrument, users must select the file of the instrument they wish to upload from their computer by clicking the “Browse” button.
-Once the user has selected the file, they must press “Install Instrument” to finalize the installment of the instrument in LORIS.
\ No newline at end of file
+The instrument manager provides the ability to view the status of
+instruments that are installed in LORIS and, if configured, the
+ability to install new instruments created with the instrument
+builder.
+
+If configured, the top of the page displays an "Upload Instrument"
+section where a user can click "Browse" to select a LINST file from
+their computer. After selecting a valid LINST file, clicking
+"Install Instrument" will install the instrument into LORIS (but
+it must still be manually inserted into a study specific test battery.)
+
+## Viewing Installed Instruments
+
+Below the "Upload Instrument" section is a table displaying all
+instruments which are currently installed in LORIS. For each
+instrument it displays the columns:
+
+*Instrument*: This provides the name of the instrument (as given
+in the `test_names` table).
+
+*Instrument Type*: This contains either "PHP" or "Instrument Builder",
+depending on if the instrument was created using the instrument
+builder or coded in PHP.
+
+*Table Installed*: This does a very basic check for each instruments
+and displays either "Exists" (or the instrument table exists in the
+LORIS SQL database) or "Missing" (if the instrument SQL table does
+not exist).
+
+*Table Valid*: This column displays whether the LORIS SQL database
+is correctly configured, with respect to the instrument that's
+installed in the LORIS `project/instruments` directory. It contains
+either of the following:
+ - "Appears Valid" if everything looks peachy
+ - "Column (name) invalid" if the SQL table exists, but the column
+ named "(name)" does not reflect the type as configured in the
+ instrument
+ - "enum (name) invalid" if the column exists, but does not accurately
+ reflect the possible options in a select box
+ - "?" if it's unable to determine the validity of the table.
+
+*Pages Valid*: This column checks whether the `instrument_subtests`
+table in LORIS is correctly populated by verifying it against the
+instrument which is installed in LORIS. It contains either "Appears
+Valid" or "Missing page (pagename)" if a page is missing from the
+`instrument_subtests` table.
+
+Note that the last two columns can only be determined for LINST
+(instrument builder) instruments as there is no way to extract the
+"proper" values from instruments coded in PHP.
\ No newline at end of file
diff --git a/modules/instrument_manager/php/instrument_manager.class.inc b/modules/instrument_manager/php/instrument_manager.class.inc
index eb1a6d27084..16aef7b4a6f 100644
--- a/modules/instrument_manager/php/instrument_manager.class.inc
+++ b/modules/instrument_manager/php/instrument_manager.class.inc
@@ -88,7 +88,10 @@ class Instrument_Manager extends \NDB_Menu_Filter
);
if ($exists <= 0) {
$db_config = $config->getSetting('database');
- exec("php generate_tables_sql_and_testNames.php < $new_file");
+ exec(
+ 'php generate_tables_sql_and_testNames.php < '
+ . escapeshellarg($new_file)
+ );
$instrument = \NDB_BVL_Instrument::factory(
$instname,
@@ -108,7 +111,7 @@ class Instrument_Manager extends \NDB_Menu_Filter
" -p" . escapeshellarg($db_config['quatPassword']).
" " . escapeshellarg($db_config['database']).
" < " . $this->path . "project/tables_sql/".
- $instrument->table.".sql"
+ escapeshellarg($instrument->table . '.sql')
);
}
}
diff --git a/modules/issue_tracker/ajax/EditIssue.php b/modules/issue_tracker/ajax/EditIssue.php
index 17d109d6df1..f222e86b7eb 100644
--- a/modules/issue_tracker/ajax/EditIssue.php
+++ b/modules/issue_tracker/ajax/EditIssue.php
@@ -429,8 +429,7 @@ function getComments($issueID)
"FROM issues_history where issueID=:issueID " .
"UNION " .
"SELECT issueComment, 'comment', dateAdded, addedBy " .
- "FROM issues_comments where issueID=:issueID " .
- "ORDER BY dateAdded DESC",
+ "FROM issues_comments where issueID=:issueID ",
array('issueID' => $issueID)
);
@@ -496,7 +495,7 @@ function emailUser($issueID, $changed_assignee)
);
$msg_data['url'] = $baseurl .
- "/issue_tracker/issue/?backURL=/issue_tracker/&issueID=" . $issueID;
+ "/issue_tracker/issue/?issueID=" . $issueID;
$msg_data['issueID'] = $issueID;
$msg_data['currentUser'] = $user->getUsername();
$msg_data['title'] = $title;
@@ -534,7 +533,7 @@ function emailUser($issueID, $changed_assignee)
);
$msg_data['url'] = $baseurl .
- "/issue_tracker/issue/?backURL=/issue_tracker/&issueID=" . $issueID;
+ "/issue_tracker/issue/?issueID=" . $issueID;
$msg_data['issueID'] = $issueID;
$msg_data['currentUser'] = $user->getUsername();
diff --git a/modules/issue_tracker/css/issue_tracker.css b/modules/issue_tracker/css/issue_tracker.css
index a74dd977623..e1bf02d1c30 100644
--- a/modules/issue_tracker/css/issue_tracker.css
+++ b/modules/issue_tracker/css/issue_tracker.css
@@ -72,3 +72,11 @@ h3 {
.page-issue-tracker h3 {
margin: 25px 0;
}
+
+.history-item-label > span {
+ font-weight: bold;
+}
+
+.history-item-changes {
+ margin-left: 2em;
+}
diff --git a/modules/issue_tracker/js/columnFormatter.js b/modules/issue_tracker/js/columnFormatter.js
index 142d22cc83e..8fbb5cc0399 100644
--- a/modules/issue_tracker/js/columnFormatter.js
+++ b/modules/issue_tracker/js/columnFormatter.js
@@ -1,2 +1,2 @@
-!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports){"use strict";function formatColumn(column,cell,rowData,rowHeaders){if(loris.hiddenHeaders.indexOf(column)>-1)return null;var row={};if(rowHeaders.forEach(function(header,index){row[header]=rowData[index]},this),"Title"===column){var cellLinks=[];return cellLinks.push(React.createElement("a",{href:loris.BaseURL+"/issue_tracker/issue/?issueID="+row["Issue ID"]+"&backURL"},row.Title)),React.createElement("td",null,cellLinks)}if("Issue ID"===column){var _cellLinks=[];return _cellLinks.push(React.createElement("a",{href:loris.BaseURL+"/issue_tracker/issue/?issueID="+row["Issue ID"]+"&backURL"},cell)),React.createElement("td",null,_cellLinks)}if("Priority"===column)switch(cell){case"normal":return React.createElement("td",{style:{background:"#CCFFCC"}},"Normal");case"high":return React.createElement("td",{style:{background:"#EEEEAA"}},"High");case"urgent":return React.createElement("td",{style:{background:"#CC6600"}},"Urgent");case"immediate":return React.createElement("td",{style:{background:"#E4A09E"}},"Immediate");case"low":return React.createElement("td",{style:{background:"#99CCFF"}},"Low");default:return React.createElement("td",null,"None")}if("PSCID"===column&&null!==row.PSCID){var _cellLinks2=[];return _cellLinks2.push(React.createElement("a",{href:loris.BaseURL+"/"+row.CandID+"/"},cell)),React.createElement("td",null,_cellLinks2)}if("Visit Label"===column&&null!==row["Visit Label"]){var _cellLinks3=[];return _cellLinks3.push(React.createElement("a",{href:loris.BaseURL+"/instrument_list/?candID="+row.CandID+"&sessionID="+row.SessionID},cell)),React.createElement("td",null,_cellLinks3)}return React.createElement("td",null,cell)}Object.defineProperty(exports,"__esModule",{value:!0}),window.formatColumn=formatColumn,exports.default=formatColumn}]);
+!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports){"use strict";function formatColumn(column,cell,rowData,rowHeaders){if(loris.hiddenHeaders.indexOf(column)>-1)return null;var row={};if(rowHeaders.forEach(function(header,index){row[header]=rowData[index]},this),"Title"===column){var cellLinks=[];return cellLinks.push(React.createElement("a",{href:loris.BaseURL+"/issue_tracker/issue/?issueID="+row["Issue ID"]},row.Title)),React.createElement("td",null,cellLinks)}if("Issue ID"===column){var _cellLinks=[];return _cellLinks.push(React.createElement("a",{href:loris.BaseURL+"/issue_tracker/issue/?issueID="+row["Issue ID"]},cell)),React.createElement("td",null,_cellLinks)}if("Priority"===column)switch(cell){case"normal":return React.createElement("td",{style:{background:"#CCFFCC"}},"Normal");case"high":return React.createElement("td",{style:{background:"#EEEEAA"}},"High");case"urgent":return React.createElement("td",{style:{background:"#CC6600"}},"Urgent");case"immediate":return React.createElement("td",{style:{background:"#E4A09E"}},"Immediate");case"low":return React.createElement("td",{style:{background:"#99CCFF"}},"Low");default:return React.createElement("td",null,"None")}if("PSCID"===column&&null!==row.PSCID){var _cellLinks2=[];return _cellLinks2.push(React.createElement("a",{href:loris.BaseURL+"/"+row.CandID+"/"},cell)),React.createElement("td",null,_cellLinks2)}if("Visit Label"===column&&null!==row["Visit Label"]){var _cellLinks3=[];return _cellLinks3.push(React.createElement("a",{href:loris.BaseURL+"/instrument_list/?candID="+row.CandID+"&sessionID="+row.SessionID},cell)),React.createElement("td",null,_cellLinks3)}return React.createElement("td",null,cell)}Object.defineProperty(exports,"__esModule",{value:!0}),window.formatColumn=formatColumn,exports.default=formatColumn}]);
//# sourceMappingURL=columnFormatter.js.map
\ No newline at end of file
diff --git a/modules/issue_tracker/js/index.js b/modules/issue_tracker/js/index.js
index d5e8d4083a5..e1ee90eadc7 100644
--- a/modules/issue_tracker/js/index.js
+++ b/modules/issue_tracker/js/index.js
@@ -1,2 +1,2 @@
-!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}({0:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}var _IssueForm=__webpack_require__(20),_IssueForm2=_interopRequireDefault(_IssueForm);$(function(){var args=QueryString.get(),issueTracker=React.createElement("div",{className:"page-issue-tracker"},React.createElement(_IssueForm2.default,{Module:"issue_tracker",DataURL:loris.BaseURL+"/issue_tracker/ajax/EditIssue.php?action=getData&issueID="+args.issueID,action:loris.BaseURL+"/issue_tracker/ajax/EditIssue.php?action=edit"}));ReactDOM.render(issueTracker,document.getElementById("lorisworkspace"))})},20:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i
- [{commentHistory[commentID].dateAdded}]
- {commentHistory[commentID].addedBy}
- {action}
- {commentHistory[commentID].newValue}
+ const changes = this.props.commentHistory.reduce(function(carry, item) {
+ let label = item.dateAdded.concat(" - ", item.addedBy);
+ if (!carry[label]) {
+ carry[label] = {};
+ }
+ carry[label][item.fieldChanged] = item.newValue;
+ return carry;
+ }, {});
+
+ const history = Object.keys(changes).sort().reverse().map(function(key, i) {
+ const textItems = Object.keys(changes[key]).map(function(index, j) {
+ return (
+
+
+
{changes[key][index]}
);
- }
- }
+ }, this);
+
+ return (
+
+
+
+ {key} updated :
+
+
+ {textItems}
+
+
+ );
+ }, this);
return (
@@ -52,7 +68,7 @@ class CommentList extends React.Component {
{btnCommentsLabel}
);
diff --git a/modules/issue_tracker/jsx/columnFormatter.js b/modules/issue_tracker/jsx/columnFormatter.js
index fa37d7b6800..aa31e242405 100644
--- a/modules/issue_tracker/jsx/columnFormatter.js
+++ b/modules/issue_tracker/jsx/columnFormatter.js
@@ -27,7 +27,7 @@ function formatColumn(column, cell, rowData, rowHeaders) {
let cellLinks = [];
cellLinks.push(
+ row['Issue ID']}>
{row.Title}
);
@@ -42,7 +42,7 @@ function formatColumn(column, cell, rowData, rowHeaders) {
let cellLinks = [];
cellLinks.push(
+ row['Issue ID']}>
{cell}
);
diff --git a/modules/issue_tracker/php/issue.class.inc b/modules/issue_tracker/php/issue.class.inc
index 5fd41e5b357..4b4973336d8 100644
--- a/modules/issue_tracker/php/issue.class.inc
+++ b/modules/issue_tracker/php/issue.class.inc
@@ -31,8 +31,7 @@ class Issue extends \NDB_Form
/**
* Override default behaviour, since the page is loaded from issueIndex.js
*
- * @return void
- * @access public
+ * @return string
*/
function display()
{
diff --git a/modules/issue_tracker/php/issue_tracker.class.inc b/modules/issue_tracker/php/issue_tracker.class.inc
index 6508bd49096..8801e792fc5 100644
--- a/modules/issue_tracker/php/issue_tracker.class.inc
+++ b/modules/issue_tracker/php/issue_tracker.class.inc
@@ -284,8 +284,6 @@ WHERE Parent IS NOT NULL ORDER BY Label",
$this->addBasicDate('minDate', 'Created After', $dateOptions);
$this->addBasicDate('maxDate', 'Created Before', $dateOptions);
$this->addCheckbox('watching', 'Watching', array('value' => '1'));
-
- $this->tpl_data['backURL'] = $_SERVER['REQUEST_URI'];
}
@@ -297,7 +295,7 @@ WHERE Parent IS NOT NULL ORDER BY Label",
* @param string $field filter field
* @param string $val filter value
*
- * @return null
+ * @return string
*/
function _addValidFilters($prepared_key, $field, $val)
{
@@ -341,7 +339,7 @@ WHERE Parent IS NOT NULL ORDER BY Label",
* will be used for the Navigation Links in the viewSession
* page.
*
- * @return associative array
+ * @return array
*/
function toArray()
{
diff --git a/modules/issue_tracker/php/my_issue_tracker.class.inc b/modules/issue_tracker/php/my_issue_tracker.class.inc
index d730c286f2e..687a755d68f 100644
--- a/modules/issue_tracker/php/my_issue_tracker.class.inc
+++ b/modules/issue_tracker/php/my_issue_tracker.class.inc
@@ -35,7 +35,7 @@ class My_Issue_Tracker extends \NDB_Menu_Filter_Form
* Set up the variables required by NDB_Menu_Filter class for constructing
* a query
*
- * @return null
+ * @return void
*/
function _setupVariables()
{
@@ -271,8 +271,6 @@ WHERE Parent IS NOT NULL ORDER BY Label",
$this->addBasicDate('minDate', 'Created After', $dateOptions);
$this->addBasicDate('maxDate', 'Created Before', $dateOptions);
$this->addCheckbox('watching', 'Watching', array('value' => '1'));
-
- $this->tpl_data['backURL'] = $_SERVER['REQUEST_URI'];
}
@@ -284,7 +282,7 @@ WHERE Parent IS NOT NULL ORDER BY Label",
* @param string $field filter field
* @param string $val filter value
*
- * @return null
+ * @return string
*/
function _addValidFilters($prepared_key, $field, $val)
{
@@ -350,7 +348,7 @@ WHERE Parent IS NOT NULL ORDER BY Label",
* will be used for the Navigation Links in the viewSession
* page.
*
- * @return associative array
+ * @return array
*/
function toArray()
{
diff --git a/modules/issue_tracker/php/resolved_issue_tracker.class.inc b/modules/issue_tracker/php/resolved_issue_tracker.class.inc
index a290a12621a..a1a4ca78802 100644
--- a/modules/issue_tracker/php/resolved_issue_tracker.class.inc
+++ b/modules/issue_tracker/php/resolved_issue_tracker.class.inc
@@ -4,7 +4,7 @@
* This class features the code for the menu portion of the Loris issue
* tracker.
*
- * PHP Version 5
+ * PHP Version 7
*
* @category Loris
* @package Main
@@ -34,7 +34,7 @@ class Resolved_Issue_Tracker extends \NDB_Menu_Filter_Form
* Set up the variables required by NDB_Menu_Filter class for constructing
* a query
*
- * @return null
+ * @return void
*/
function _setupVariables()
{
@@ -272,8 +272,6 @@ WHERE Parent IS NOT NULL ORDER BY Label ",
$this->addBasicDate('minDate', 'Created After', $dateOptions);
$this->addBasicDate('maxDate', 'Created Before', $dateOptions);
$this->addCheckbox('watching', 'Watching', array('value' => '1'));
-
- $this->tpl_data['backURL'] = $_SERVER['REQUEST_URI'];
}
@@ -285,7 +283,7 @@ WHERE Parent IS NOT NULL ORDER BY Label ",
* @param string $field filter field
* @param string $val filter value
*
- * @return null
+ * @return string
*/
function _addValidFilters($prepared_key, $field, $val)
{
@@ -330,7 +328,7 @@ WHERE Parent IS NOT NULL ORDER BY Label ",
* will be used for the Navigation Links in the viewSession
* page.
*
- * @return associative array
+ * @return array
*/
function toArray()
{
diff --git a/modules/issue_tracker/templates/issue_tracker_controlpanel.tpl b/modules/issue_tracker/templates/issue_tracker_controlpanel.tpl
index 88348b1d908..005a3e93a7e 100644
--- a/modules/issue_tracker/templates/issue_tracker_controlpanel.tpl
+++ b/modules/issue_tracker/templates/issue_tracker_controlpanel.tpl
@@ -1,12 +1,13 @@
Navigation
{if $issue.backURL}
-
-
- Back to list
-
-
-
+
+
+
+ Back to list
+
+
+
{/if}
{if $issue.prevIssue.URL != ''}
diff --git a/modules/login/php/passwordexpiry.class.inc b/modules/login/php/passwordexpiry.class.inc
index ea34dcfd04d..e183829e517 100644
--- a/modules/login/php/passwordexpiry.class.inc
+++ b/modules/login/php/passwordexpiry.class.inc
@@ -146,7 +146,7 @@ class PasswordExpiry extends \NDB_Form
*
* @param array $values The values that were submitted to the page.
*
- * @return none
+ * @return void
*/
function _process($values)
{
diff --git a/modules/media/README.md b/modules/media/README.md
index 88caa226c07..644a9b3756f 100644
--- a/modules/media/README.md
+++ b/modules/media/README.md
@@ -1,51 +1,58 @@
-## Media Module
+# Media
-### 📄 Overview
+## Purpose
-Media module allows users to **upload**, **search** and **edit** media files associated with a specific candidate timepoint in Loris.
-Any kind of data associated with a candidate timepoint can be uploaded through this module: PDFs, videos, recordings, scripts, log files, etc. Files can optionally be associated to a specific instrument form within a given candidate timepoint.
+Media module allows users to **upload**, **search** and **edit** media files
+associated with a specific candidate timepoint in Loris. Any kind of data
+associated with a candidate timepoint can be uploaded through this module:
+PDFs, videos, recordings, scripts, log files, etc. Files can optionally be
+associated with a specific instrument from within a given candidate timepoint.
+## Intended Users
->Note: Currently editing functionality only allows editing of certain metadata fields, such as `Comments` and `Date of Administration`.
+The Media module is used by data entry staff to upload candidate and timepoint
+specific media. The module can also be used by data-monitoring staff ensuring the
+integrity of the data for each candidate.
-### 🔒 Permissions
+## Scope
-In order to use the media module the user needs one or both of the following permissions:
+The Media module provides a tool for uploading files to the server and tracking
+them, once uploaded. Data uploaded must be specific to a candidate and timepoint,
+and, optionally, an instrument. The *Edit* functionality only allows modification of
+the date of upload and of comments linked to the upload. The *Delete* functionality
+only hides the file from the front-end; it does not remove its database entry nor
+the file on the server.
-1. **media_read** - gives user a read-only access to media module (file browsing only)
-2. **media_write** - gives user a write access to media module (upload/delete files and edit metadata)
+Out of scope: media that is not affiliated with a candidate does not belong in Media.
->**Note**: superusers have both of the aforementioned permissions by default! 💪
+## Permissions
-### :file_folder: Upload path
+In order to use the media module the user needs one or both of the following
+permissions:
-By default, all files are uploaded under `/data/uploads/`.
-*(Note this directory is not created by the Loris install script and should be manually created by the admin.)*
+- `media_read`: gives user a read-only access to media module
+(file browsing only)
+- `media_write`: gives user a write access to media module
+(upload/delete files and edit metadata)
-The upload path is configurable in `Paths` section of `Configuration` module.
+Media files are displayed if and only if the files were uploaded to a site within
+the logged in user's own site affiliations.
->**Important**
->
->The upload path must be readable and writable by your web server; either the web server `user` or `group` must have read and write permissions.
->The default group for your web server process is listed below
->```
->Ubuntu: $GROUP$ = www-data
->CentOS: $GROUP$ = apache
->```
->
->To find the `user` of your web server, run `ps aux | grep 'apache' | egrep -v 'grep|Ss' | awk '{ print $1 }' | sort | uniq`
->To find the `group` of your web server, run `ps aux | grep 'apache' | egrep -v 'grep|Ss' | awk '{ print $1 }' | sort | uniq | groups`
+## Configurations
->To see if your web server's user or group owns the upload path, run `ls -ld /data/uploads | awk '{ print "user:" $3 ", group:" $4 }'`
+The following configuration is necessary for the media module to function
->If neither owns the folder, you should run `sudo chown : /data/uploads`
->Then, run `chmod 775 /data/uploads`
+- `mediaPath`: determine where files will be uploaded on the server. Files are
+uploaded directly into the directory specified (no subdirectories are created).
-### 💯 Features
+Creation of the upload directory is not done by the install script automatically and
+permissions for access to the directory must be set-up manually, either the web
+server `user` or `group` must have read and write permissions.
-1. **Browse** a list of uploaded files and related information
-2. **Edit** metadata about media files (except timepoint related data such as PSCID, Visit Label and Instrument)
-3. **Upload** new files associated to a specific timepoint
- - PSCID, Visit Label and Site are required fields for all uploaded files
- - File name should always start with [PSCID]\_[Visit Label]\_[Instrument] corresponding to the selection in the upload form
-4. **Delete** files. Deleting a file hides it from the frontend, but preserves a copy in the database
+## Interactions with LORIS
+
+Media module depends on a session to be already created before any files can be
+uploaded for it.
+
+Uploaded files are displayed in the browse tab. Each entry has a link to download
+the file itself and a link to the timepoint the file was uploaded for.
diff --git a/modules/media/ajax/FileUpload.php b/modules/media/ajax/FileUpload.php
index ead99197256..a29213932be 100644
--- a/modules/media/ajax/FileUpload.php
+++ b/modules/media/ajax/FileUpload.php
@@ -106,6 +106,7 @@ function uploadFile()
$site = isset($_POST['forSite']) ? $_POST['forSite'] : null;
$dateTaken = isset($_POST['dateTaken']) ? $_POST['dateTaken'] : null;
$comments = isset($_POST['comments']) ? $_POST['comments'] : null;
+ $language = isset($_POST['language']) ? $_POST['language'] : null;
// If required fields are not set, show an error
if (!isset($_FILES) || !isset($pscid) || !isset($visit) || !isset($site)) {
@@ -155,6 +156,7 @@ function uploadFile()
'uploaded_by' => $userID,
'hide_file' => 0,
'date_uploaded' => date("Y-m-d H:i:s"),
+ 'language_id' => $language,
];
if (move_uploaded_file($_FILES["file"]["tmp_name"], $mediaPath . $fileName)) {
@@ -201,6 +203,7 @@ function getUploadFields()
$candIdList = toSelect($candidates, "CandID", "PSCID");
$visitList = Utility::getVisitList();
$siteList = Utility::getSiteList(false);
+ $languageList = Utility::getLanguageList();
// Build array of session data to be used in upload media dropdowns
$sessionData = [];
@@ -291,6 +294,7 @@ function getUploadFields()
"comments, " .
"file_name as fileName, " .
"hide_file as hideFile, " .
+ "language_id as language," .
"m.id FROM media m LEFT JOIN session s ON m.session_id = s.ID " .
"WHERE m.id = $idMediaFile",
[]
@@ -306,6 +310,7 @@ function getUploadFields()
'mediaData' => $mediaData,
'mediaFiles' => array_values(getFilesList()),
'sessionData' => $sessionData,
+ 'language' => $languageList,
];
return $result;
diff --git a/modules/media/help/Setup.md b/modules/media/help/Setup.md
new file mode 100644
index 00000000000..603996bf98c
--- /dev/null
+++ b/modules/media/help/Setup.md
@@ -0,0 +1,33 @@
+## Media module set up
+
+Set the `mediaPath` config value to the desired path on the server where the files
+will be uploaded.
+
+Create the directory on the server.
+
+The upload path must be readable and writable by your web server;
+either the web server `user` or `group` must have read and write permissions.
+
+The default group for your web server process is listed below:
+```
+Ubuntu: $GROUP$ = www-data
+CentOS: $GROUP$ = apache
+```
+
+To find the `user` of your web server, run:
+
+`ps aux | grep 'apache' | egrep -v 'grep|Ss' | awk '{ print $1 }' | sort | uniq`
+
+To find the `group` of your web server, run:
+
+`ps aux | grep 'apache' | egrep -v 'grep|Ss' | awk '{ print $1 }' | sort | uniq | xargs groups`
+
+To see if your web server's user or group owns the upload path, run:
+
+`ls -ld /data/uploads | awk '{ print "user:" $3 ", group:" $4 }'`
+
+If neither owns the folder, you should run the following two commands:
+```
+sudo chown : /data/uploads
+sudo chmod 775 /data/uploads
+```
diff --git a/modules/media/help/media.md b/modules/media/help/media.md
index dd14d08a4d2..8a2796b81f8 100644
--- a/modules/media/help/media.md
+++ b/modules/media/help/media.md
@@ -1,51 +1,43 @@
-# Media Module
+# Media
-## Overview
+The Media module serves as a repository for files associated with a particular
+candidate-timepoint or instrument in a study. Users can access and upload files
+such as PDF scans, recordings, log files, or stimulus presentation media in multiple
+formats such as .pdf, .mp4, .mp3, .txt, etc.
-Media module allows users to **upload**, **search** and **edit** media files associated with a specific candidate timepoint in Loris.
-Any kind of data associated with a candidate timepoint can be uploaded through this module: PDFs, videos, recordings, scripts, log files, etc. Files can optionally be associated to a specific instrument form within a given candidate timepoint.
+## Searching for a Media File
+Under the Browse tab, use the Selection Filters to search for files by fields such
+as filename, file type, candidate PSCID, Visit Label, Instrument, Site, or
+name of user who uploaded the file. Partial string matching can be used on many
+fields (e.g. filename). As filters are selected, the data table below will
+dynamically update with relevant results. Click the “Clear Filters” button to reset
+all filters.
->Note: Currently editing functionality only allows editing of certain metadata fields, such as `Comments` and `Date of Administration`.
+Within the data table, results can be sorted in ascending or descending order by
+clicking on any column header. To download a file, click on the filename (in blue
+text).
-## Permissions
+## Uploading a Media File
-In order to use the media module the user needs one or both of the following permissions:
+Under the “Upload” tab, users will be able to upload a new media file.
+Users must specify information about the file by selecting from a number of dropdown
+menus including PSCID, Visit Label, Site, Instrument (optional), Date of
+Administration, and enter comments on the file in the textbox provided. Lastly,
+users will select the file they wish to upload by clicking the “Browse” button and
+selecting the file from their computer. To begin the upload, users must click
+“Upload File”.
-1. **media_read** - gives user a read-only access to media module (file browsing only)
-2. **media_write** - gives user a write access to media module (upload/delete files and edit metadata)
+_Note that a file to be uploaded should follow the naming convention listed at the
+top of the Upload page: [PSCID]\_[VisitLabel]\_[Instrument]_
->**Note**: superusers have both of the aforementioned permissions by default!
+## Editing a Media File
-## :file_folder: Upload path
+Click “Edit” from the “Edit Metadata” column to update or edit certain file
+properties, via the “Edit Media File” page.
-By default, all files are uploaded under `/data/uploads/`.
-*(Note this directory is not created by the Loris install script and should be manually created by the admin.)*
-
-The upload path is configurable in `Paths` section of `Configuration` module.
-
->**Important**
->
->The upload path must be readable and writable by your web server; either the web server `user` or `group` must have read and write permissions.
->The default group for your web server process is listed below
->```
->Ubuntu: $GROUP$ = www-data
->CentOS: $GROUP$ = apache
->```
->
->To find the `user` of your web server, run `ps aux | grep 'apache' | egrep -v 'grep|Ss' | awk '{ print $1 }' | sort | uniq`
->To find the `group` of your web server, run `ps aux | grep 'apache' | egrep -v 'grep|Ss' | awk '{ print $1 }' | sort | uniq | groups`
-
->To see if your web server's user or group owns the upload path, run `ls -ld /data/uploads | awk '{ print "user:" $3 ", group:" $4 }'`
-
->If neither owns the folder, you should run `sudo chown : /data/uploads`
->Then, run `chmod 775 /data/uploads`
-
-## Features
-
-1. **Browse** a list of uploaded files and related information
-2. **Edit** metadata about media files (except timepoint related data such as PSCID, Visit Label and Instrument)
-3. **Upload** new files associated to a specific timepoint
- - PSCID, Visit Label and Site are required fields for all uploaded files
- - File name should always start with [PSCID]\_[Visit Label]\_[Instrument] corresponding to the selection in the upload form
-4. **Delete** files. Deleting a file hides it from the frontend, but preserves a copy in the database
\ No newline at end of file
+This page allows users to modify information about an uploaded file. Fields such
+as Date of Administration, Comments, and file visibility (hidden or visible) can be
+updated. Note that fields such as PSCID, Visit Label, Site, and Instrument cannot
+be modified on uploaded media files. To finalize an update, users must click “Update
+File”.
diff --git a/modules/media/js/mediaIndex.js b/modules/media/js/mediaIndex.js
index 13e305b9a09..6ec624a1083 100644
--- a/modules/media/js/mediaIndex.js
+++ b/modules/media/js/mediaIndex.js
@@ -1,2 +1,3 @@
-!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}({0:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}var _createClass=function(){function defineProperties(target,props){for(var i=0;i0&&(activeTab=_this.props.tabs[0].id),_this.state={activeTab:activeTab},_this.handleClick=_this.handleClick.bind(_this),_this.getTabs=_this.getTabs.bind(_this),_this.getTabPanes=_this.getTabPanes.bind(_this),_this}return _inherits(Tabs,_React$Component),_createClass(Tabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("ul",{className:"nav nav-tabs",role:"tablist",style:tabStyle},tabs),React.createElement("div",{className:"tab-content"},tabPanes))}}]),Tabs}(React.Component);Tabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},Tabs.defaultProps={onTabChange:function(){},updateURL:!1};var TabPane=function(_React$Component2){function TabPane(){return _classCallCheck(this,TabPane),_possibleConstructorReturn(this,(TabPane.__proto__||Object.getPrototypeOf(TabPane)).apply(this,arguments))}return _inherits(TabPane,_React$Component2),_createClass(TabPane,[{key:"render",value:function(){var classList="tab-pane",title=void 0;return this.props.TabId===this.props.activeTab&&(classList+=" active"),this.props.Title&&(title=React.createElement("h1",null,this.props.Title)),React.createElement("div",{role:"tabpanel",className:classList,id:this.props.TabId},title,this.props.children)}}]),TabPane}(React.Component);TabPane.propTypes={TabId:React.PropTypes.string.isRequired,Title:React.PropTypes.string,activeTab:React.PropTypes.string},exports.Tabs=Tabs,exports.TabPane=TabPane},13:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i=0?swal({title:"Are you sure?",text:"A file with this name already exists!\n Would you like to override existing file?",type:"warning",showCancelButton:!0,confirmButtonText:"Yes, I am sure!",cancelButtonText:"No, cancel it!"},function(isConfirm){isConfirm?this.uploadFile():swal("Cancelled","Your imaginary file is safe :)","error")}.bind(this)):this.uploadFile()}}},{key:"uploadFile",value:function(){var formData=this.state.formData,formObj=new FormData;for(var key in formData)""!==formData[key]&&formObj.append(key,formData[key]);$.ajax({type:"POST",url:this.props.action,data:formObj,cache:!1,contentType:!1,processData:!1,xhr:function(){var xhr=new window.XMLHttpRequest;return xhr.upload.addEventListener("progress",function(evt){if(evt.lengthComputable){var percentage=Math.round(evt.loaded/evt.total*100);this.setState({uploadProgress:percentage})}}.bind(this),!1),xhr}.bind(this),success:function(){var mediaFiles=JSON.parse(JSON.stringify(this.state.Data.mediaFiles));mediaFiles.push(formData.file.name);var event=new CustomEvent("update-datatable");window.dispatchEvent(event),this.setState({mediaFiles:mediaFiles,formData:{},uploadProgress:-1}),swal("Upload Successful!","","success")}.bind(this),error:function(err){console.error(err);var msg=err.responseJSON?err.responseJSON.message:"Upload error!";this.setState({errorMessage:msg,uploadProgress:-1}),swal(msg,"","error")}.bind(this)})}},{key:"isValidFileName",value:function(requiredFileName,fileName){return null!==fileName&&null!==requiredFileName&&0===fileName.indexOf(requiredFileName)}},{key:"isValidForm",value:function isValidForm(formRefs,formData){var isValidForm=!0,requiredFields={pscid:null,visitLabel:null,file:null};return Object.keys(requiredFields).map(function(field){formData[field]?requiredFields[field]=formData[field]:formRefs[field]&&(formRefs[field].props.hasError=!0,isValidForm=!1)}),this.forceUpdate(),isValidForm}},{key:"setFormData",value:function(formElement,value){var visitLabel=this.state.formData.visitLabel,pscid=this.state.formData.pscid;"pscid"===formElement&&""!==value&&(this.state.Data.visits=this.state.Data.sessionData[value].visits,this.state.Data.sites=this.state.Data.sessionData[value].sites,visitLabel?this.state.Data.instruments=this.state.Data.sessionData[value].instruments[visitLabel]:this.state.Data.instruments=this.state.Data.sessionData[value].instruments.all),"visitLabel"===formElement&&""!==value&&pscid&&(this.state.Data.instruments=this.state.Data.sessionData[pscid].instruments[value]);var formData=this.state.formData;formData[formElement]=value,this.setState({formData:formData})}}]),MediaUploadForm}(React.Component);MediaUploadForm.propTypes={DataURL:React.PropTypes.string.isRequired,action:React.PropTypes.string.isRequired},exports.default=MediaUploadForm},24:function(module,exports){"use strict";function formatColumn(column,cell,rowData,rowHeaders){if(loris.hiddenHeaders.indexOf(column)>-1)return null;var row={};rowHeaders.forEach(function(header,index){row[header]=rowData[index]},this);var classes=[];"1"===row["Hide File"]&&classes.push("bg-danger"),classes=classes.join(" ");var hasWritePermission=loris.userHasPermission("media_write");if("File Name"===column&&hasWritePermission===!0){var downloadURL=loris.BaseURL+"/media/ajax/FileDownload.php?File="+row["File Name"];return React.createElement("td",{className:classes},React.createElement("a",{href:downloadURL,target:"_blank",download:row["File Name"]},cell))}if("Visit Label"===column&&null!==row["Cand ID"]&&row["Session ID"]){var sessionURL=loris.BaseURL+"/instrument_list/?candID="+row["Cand ID"]+"&sessionID="+row["Session ID"];return React.createElement("td",{className:classes},React.createElement("a",{href:sessionURL},cell))}if("Edit Metadata"===column){var editURL=loris.BaseURL+"/media/edit/?id="+row["Edit Metadata"];return React.createElement("td",{className:classes},React.createElement("a",{href:editURL},"Edit"))}return React.createElement("td",{className:classes},cell)}Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=formatColumn}});
+!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}({0:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}var _createClass=function(){function defineProperties(target,props){for(var i=0;i0&&(activeTab=_this.props.tabs[0].id),_this.state={activeTab:activeTab},_this.handleClick=_this.handleClick.bind(_this),_this.getTabs=_this.getTabs.bind(_this),_this.getTabPanes=_this.getTabPanes.bind(_this),_this}return _inherits(Tabs,_React$Component),_createClass(Tabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("ul",{className:"nav nav-tabs",role:"tablist",style:tabStyle},tabs),React.createElement("div",{className:"tab-content"},tabPanes))}}]),Tabs}(React.Component);Tabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},Tabs.defaultProps={onTabChange:function(){},updateURL:!0};var VerticalTabs=function(_React$Component2){function VerticalTabs(props){_classCallCheck(this,VerticalTabs);var _this2=_possibleConstructorReturn(this,(VerticalTabs.__proto__||Object.getPrototypeOf(VerticalTabs)).call(this,props)),hash=window.location.hash,activeTab="";return _this2.props.updateURL&&hash?activeTab=hash.substr(1):_this2.props.defaultTab?activeTab=_this2.props.defaultTab:_this2.props.tabs.length>0&&(activeTab=_this2.props.tabs[0].id),_this2.state={activeTab:activeTab},_this2.handleClick=_this2.handleClick.bind(_this2),_this2.getTabs=_this2.getTabs.bind(_this2),_this2.getTabPanes=_this2.getTabPanes.bind(_this2),_this2}return _inherits(VerticalTabs,_React$Component2),_createClass(VerticalTabs,[{key:"handleClick",value:function(tabId,e){if(this.setState({activeTab:tabId}),this.props.onTabChange(tabId),this.props.updateURL){var scrollDistance=$("body").scrollTop()||$("html").scrollTop();window.location.hash=e.target.hash,$("html,body").scrollTop(scrollDistance)}}},{key:"getTabs",value:function(){var tabs=this.props.tabs.map(function(tab){var tabClass=this.state.activeTab===tab.id?"active":null,href="#"+tab.id,tabID="tab-"+tab.id;return React.createElement("li",{role:"presentation",className:tabClass,key:tab.id},React.createElement("a",{id:tabID,href:href,role:"tab","data-toggle":"tab",onClick:this.handleClick.bind(null,tab.id)},tab.label))}.bind(this));return tabs}},{key:"getTabPanes",value:function(){var tabPanes=React.Children.map(this.props.children,function(child,key){if(child)return React.cloneElement(child,{activeTab:this.state.activeTab,key:key})}.bind(this));return tabPanes}},{key:"render",value:function(){var tabs=this.getTabs(),tabPanes=this.getTabPanes(),tabStyle={marginLeft:0,marginBottom:"5px"};return React.createElement("div",null,React.createElement("div",{className:"tabbable col-md-3 col-sm-3"},React.createElement("ul",{className:"nav nav-pills nav-stacked",role:"tablist",style:tabStyle},tabs)),React.createElement("div",{className:"tab-content col-md-9 col-sm-9"},tabPanes))}}]),VerticalTabs}(React.Component);VerticalTabs.propTypes={tabs:React.PropTypes.array.isRequired,defaultTab:React.PropTypes.string,updateURL:React.PropTypes.bool},VerticalTabs.defaultProps={onTabChange:function(){},updateURL:!0};var TabPane=function(_React$Component3){function TabPane(){return _classCallCheck(this,TabPane),_possibleConstructorReturn(this,(TabPane.__proto__||Object.getPrototypeOf(TabPane)).apply(this,arguments))}return _inherits(TabPane,_React$Component3),_createClass(TabPane,[{key:"render",value:function(){var classList="tab-pane",title=void 0;return this.props.TabId===this.props.activeTab&&(classList+=" active"),this.props.Title&&(title=React.createElement("h1",null,this.props.Title)),React.createElement("div",{role:"tabpanel",className:classList,id:this.props.TabId},title,this.props.children)}}]),TabPane}(React.Component);TabPane.propTypes={TabId:React.PropTypes.string.isRequired,Title:React.PropTypes.string,activeTab:React.PropTypes.string},exports.Tabs=Tabs,exports.VerticalTabs=VerticalTabs,exports.TabPane=TabPane},13:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(obj){return typeof obj}:function(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_createClass=function(){function defineProperties(target,props){for(var i=0;i=0?swal({title:"Are you sure?",text:"A file with this name already exists!\n Would you like to override existing file?",type:"warning",showCancelButton:!0,confirmButtonText:"Yes, I am sure!",cancelButtonText:"No, cancel it!"},function(isConfirm){isConfirm?this.uploadFile():swal("Cancelled","Your imaginary file is safe :)","error")}.bind(this)):this.uploadFile()}}},{key:"uploadFile",value:function(){var formData=this.state.formData,formObj=new FormData;for(var key in formData)""!==formData[key]&&formObj.append(key,formData[key]);$.ajax({type:"POST",url:this.props.action,data:formObj,cache:!1,contentType:!1,processData:!1,xhr:function(){var xhr=new window.XMLHttpRequest;return xhr.upload.addEventListener("progress",function(evt){if(evt.lengthComputable){var percentage=Math.round(evt.loaded/evt.total*100);this.setState({uploadProgress:percentage})}}.bind(this),!1),xhr}.bind(this),success:function(){var mediaFiles=JSON.parse(JSON.stringify(this.state.Data.mediaFiles));mediaFiles.push(formData.file.name);var event=new CustomEvent("update-datatable");window.dispatchEvent(event),this.setState({mediaFiles:mediaFiles,formData:{},uploadProgress:-1}),swal("Upload Successful!","","success")}.bind(this),error:function(err){console.error(err);var msg=err.responseJSON?err.responseJSON.message:"Upload error!";this.setState({errorMessage:msg,uploadProgress:-1}),swal(msg,"","error")}.bind(this)})}},{key:"isValidFileName",value:function(requiredFileName,fileName){return null!==fileName&&null!==requiredFileName&&0===fileName.indexOf(requiredFileName)}},{key:"isValidForm",value:function isValidForm(formRefs,formData){var isValidForm=!0,requiredFields={pscid:null,visitLabel:null,file:null};return Object.keys(requiredFields).map(function(field){formData[field]?requiredFields[field]=formData[field]:formRefs[field]&&(formRefs[field].props.hasError=!0,isValidForm=!1)}),this.forceUpdate(),isValidForm}},{key:"setFormData",value:function(formElement,value){var visitLabel=this.state.formData.visitLabel,pscid=this.state.formData.pscid;"pscid"===formElement&&""!==value&&(this.state.Data.visits=this.state.Data.sessionData[value].visits,this.state.Data.sites=this.state.Data.sessionData[value].sites,visitLabel?this.state.Data.instruments=this.state.Data.sessionData[value].instruments[visitLabel]:this.state.Data.instruments=this.state.Data.sessionData[value].instruments.all),"visitLabel"===formElement&&""!==value&&pscid&&(this.state.Data.instruments=this.state.Data.sessionData[pscid].instruments[value]);var formData=this.state.formData;formData[formElement]=value,this.setState({formData:formData})}}]),MediaUploadForm}(React.Component);MediaUploadForm.propTypes={DataURL:React.PropTypes.string.isRequired,action:React.PropTypes.string.isRequired},exports.default=MediaUploadForm},24:function(module,exports){"use strict";function formatColumn(column,cell,rowData,rowHeaders){if(loris.hiddenHeaders.indexOf(column)>-1)return null;var row={};rowHeaders.forEach(function(header,index){row[header]=rowData[index]},this);var classes=[];"1"===row["Hide File"]&&classes.push("bg-danger"),classes=classes.join(" ");var hasWritePermission=loris.userHasPermission("media_write");if("File Name"===column&&hasWritePermission===!0){var downloadURL=loris.BaseURL+"/media/ajax/FileDownload.php?File="+row["File Name"];return React.createElement("td",{className:classes},React.createElement("a",{href:downloadURL,target:"_blank",download:row["File Name"]},cell))}if("Visit Label"===column&&null!==row["Cand ID"]&&row["Session ID"]){
+var sessionURL=loris.BaseURL+"/instrument_list/?candID="+row["Cand ID"]+"&sessionID="+row["Session ID"];return React.createElement("td",{className:classes},React.createElement("a",{href:sessionURL},cell))}if("Edit Metadata"===column){var editURL=loris.BaseURL+"/media/edit/?id="+row["Edit Metadata"];return React.createElement("td",{className:classes},React.createElement("a",{href:editURL},"Edit"))}return React.createElement("td",{className:classes},cell)}Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=formatColumn}});
//# sourceMappingURL=mediaIndex.js.map
\ No newline at end of file
diff --git a/modules/media/jsx/uploadForm.js b/modules/media/jsx/uploadForm.js
index 41af9f6c159..71161fb0a2d 100644
--- a/modules/media/jsx/uploadForm.js
+++ b/modules/media/jsx/uploadForm.js
@@ -118,10 +118,12 @@ class MediaUploadForm extends React.Component {
required={true}
value={this.state.formData.visitLabel}
/>
-
+
addBasicText('pSCID', 'PSCID', ["size" => 9, "maxlength" => 7]);
$this->addBasicText('fileName', 'File Name');
@@ -111,6 +118,7 @@ class Media extends \NDB_Menu_Filter
$this->addSelect('site', 'For Site', $siteList);
$this->addBasicText('uploadedBy', 'Uploaded By');
$this->addSelect('instrument', 'Instrument', $instrumentList);
+ $this->addSelect('language', 'Language', $languageList);
// Add hidden files filter is user is admin
if ($this->hasHidePermission) {
@@ -134,15 +142,17 @@ class Media extends \NDB_Menu_Filter
/**
* Build a list of media to display in Data Table
*
- * @return bool
+ * @return void
* @throws \DatabaseException
*/
function _setupVariables()
{
$user =& \User::singleton();
// the base query
- $query = " FROM media m LEFT JOIN session s ON m.session_id = s.ID".
- " LEFT JOIN candidate c ON c.CandID=s.CandID";
+ $query = " FROM media m
+ LEFT JOIN session s ON m.session_id = s.ID
+ LEFT JOIN candidate c ON c.CandID=s.CandID
+ LEFT JOIN language l USING (language_id) ";
$query .= " WHERE 1=1 ";
// set the class variables
@@ -151,6 +161,7 @@ class Media extends \NDB_Menu_Filter
'm.file_name',
'(SELECT PSCID from candidate WHERE CandID=s.CandID) as pscid',
's.Visit_label as visit_label',
+ 'l.language_label',
'(SELECT Full_name FROM test_names WHERE Test_name=m.instrument)',
'(SELECT name FROM psc WHERE CenterID=s.CenterID) as site',
'm.uploaded_by',
@@ -181,6 +192,7 @@ class Media extends \NDB_Menu_Filter
'File Name',
'PSCID',
'Visit Label',
+ 'Language',
'Instrument',
'Site',
'Uploaded By',
@@ -204,6 +216,7 @@ class Media extends \NDB_Menu_Filter
's.Visit_label',
's.CenterID',
'm.hide_file',
+ 'l.language_id',
];
$this->formToFilter = [
'pscid' => 'c.PSCID',
@@ -211,6 +224,7 @@ class Media extends \NDB_Menu_Filter
'visit_label' => 's.Visit_label',
'for_site' => 's.CenterID',
'hide_file' => 'm.hide_file',
+ 'language' => 'l.language_id',
];
return true;
}
@@ -219,7 +233,7 @@ class Media extends \NDB_Menu_Filter
* Converts the results of this menu filter to a JSON format to be retrieved
* with ?format=json
*
- * @return a json encoded string of the headers and data from this table
+ * @return string a json encoded string of the headers and data from this table
*/
function toJSON()
{
diff --git a/modules/mri_violations/README.md b/modules/mri_violations/README.md
new file mode 100644
index 00000000000..ae938ba5966
--- /dev/null
+++ b/modules/mri_violations/README.md
@@ -0,0 +1,87 @@
+# MRI violations
+
+## Purpose
+
+The MRI violations module provides a front end to view information
+about scans that were flagged as violations of the imaging protocol
+(which is configurable and set by each study) by the imaging pipeline
+scripts.
+
+The violations also serve as prompts to inform
+scanner technologists that settings need to be adjusted for future
+scans (i.e. number of slices are being reduced from the protocol), or
+as a cue that something is wrong (i.e. coil not functioning properly).
+
+## Intended Users
+
+The MRI violations module is primarily used by DCC staff and the MR
+Imaging Committee who both decide whether a scan should be included
+in the batch of study scans or, alternatively, if it should be excluded
+from further analysis.
+
+## Scope
+
+The MRI violations module shows a summary of the MRI violations
+(one image per row) identified by the imaging pipeline scripts.
+Depending on the type of violation, clicking on the scan's problem
+will redirect the user to pages with more information regarding
+the identified violation. The different types of violation are:
+
+- Could not identify scan type
+- Protocol Violation
+- Candidate Mismatch
+
+Additionally, the violations "Resolution Status" column can be
+updated once the violation has been resolved. Available options
+are:
+
+- **Unresolved**: this indicates this scan still needs to be
+addressed by the DCC or MRI committee
+- **Reran**: this indicates that this scan will be rerun through
+the imaging insertion pipeline and inserted
+- **Emailed site/pending**: this indicates that the site has been
+emailed about this scan, and a resolution or answer is pending
+(i.e.. mismatched PSCID and DCCID in the naming convention or a visit
+label that has not yet been created)
+- **Inserted**: this means that the scan has been inserted into
+the Imaging Browser, and no flag was needed
+- **Rejected**: this means that the scan has been excluded for future
+analysis and will not be inserted into the Imaging Browser nor
+available for download from the DQT
+- **Inserted with flag**: this means that the scan has been inserted
+into the Imaging Browser and a caveat flag was attached - this caveat
+flag should have either a drop down menu or open text for the person
+approving the insertion to indicate what the flag is for
+- **Other**: resolutions that don't fit in any of the above categories
+
+Finally, the `mri_protocol` table can be updated directly from
+the frontend if the user has the permission for it.
+
+## Permissions
+
+The permission `violated_scans_view_allsites` is required to access
+the MRI violated module.
+
+In addition, the permission `violated_scans_edit` allows the user to
+edit the `mri_protocol` table directly from the browser.
+
+## Configurations
+
+The `mri_protocol` and `mri_protocol_checks` tables need to be configured
+with the study MRI protocol in order for the imaging scripts to determine
+whether a scan violates the protocol set by the study.
+
+## Interactions with LORIS
+
+The `mri_protocol_violated_scans`, `mri_violations_log` and
+`MRICandidateErrors` tables used by the MRI violations module must
+be populated before there is any data in the module. The imaging
+`tarchiveLoader` script populates them as part of the imaging pipeline.
+
+**Notes:**
+- `mri_protocol_violated_scans` logs all the scans referring to
+the problem "Could not identify scan type"
+- `mri_violations_log` logs all the scans referring to the problem
+"Protocol Violation"
+- `MRICandidateErrors` logs all the scans referring to the problem
+"Candidate Mismatch"
diff --git a/modules/mri_violations/js/mri_protocol_violations.js b/modules/mri_violations/js/mri_protocol_violations.js
index a1e4ca75899..41036983ac3 100644
--- a/modules/mri_violations/js/mri_protocol_violations.js
+++ b/modules/mri_violations/js/mri_protocol_violations.js
@@ -22,41 +22,52 @@ function save() {
"use strict";
var default_value, id, value;
/**To get the default value**/
- $('.description').click(function (event) {
+ $('.description').focus(function (event) {
id = event.target.id;
default_value = $("#" + id).text();
});
- $('.description').bind('blur', function (event) {
- event.stopImmediatePropagation();
- id = event.target.id;
- value = $("#" + id).text();
- $('
').appendTo('body')
- //.css({background : 'black', opacity: '0.9'})
- .html("Are you sure?
")
- .dialog({
- title: 'Modification',
- width: 'auto',
- resizable: false,
- //dialogClass:'transparent',
- position: [800, 120],
- buttons: {
- Yes: function () {
- $.get(loris.BaseURL + "/mri_violations/ajax/UpdateMRIProtocol.php?field_id=" + id + "&field_value=" + value, function () {});
- $(this).dialog("close");
- },
- close: function () {
- $(this).remove();
- $("#" + id).text(default_value);
- }
- }
- });
- }).keypress(function (e) {
- if (e.which === 13) { // Determine if the user pressed the enter button
- e.preventDefault();
- $(this).blur();
+ $('.description').keypress(
+ function(event) {
+ if (event.which === 13 || event.keyCode === 13) {
+ event.preventDefault();
+ var id = '#' + event.target.id;
+ $(id).blur();
}
- });
+ }
+ );
+
+ $('.description').blur(
+ function(event) {
+ event.stopImmediatePropagation();
+ id = event.target.id;
+ value = $('#'+id).text();
+ if (value !== default_value) {
+ $('.description').attr('contenteditable', false);
+ swal({
+ title: "Are you sure?",
+ text: "Are you sure you want to edit this field?",
+ type: "warning",
+ showCancelButton: true,
+ confirmButtonText: 'Yes, I am sure!',
+ cancelButtonText: "No, cancel it!"
+ }, function(isConfirm) {
+ if (isConfirm) {
+ $.post(
+ loris.BaseURL + '/mri_violations/ajax/UpdateMRIProtocol.php',
+ {
+ field_id: id,
+ field_value: value
+ }
+ );
+ } else {
+ $('#' + id).text(default_value);
+ }
+ });
+ $('.description').attr('contenteditable', true);
+ }
+ }
+ );
}
$(function () {
diff --git a/modules/mri_violations/php/mri_protocol_violations.class.inc b/modules/mri_violations/php/mri_protocol_violations.class.inc
index 0321cd78de8..c4c16c7e442 100644
--- a/modules/mri_violations/php/mri_protocol_violations.class.inc
+++ b/modules/mri_violations/php/mri_protocol_violations.class.inc
@@ -46,7 +46,7 @@ class Mri_Protocol_Violations extends \NDB_Menu_Filter
/**
* Set up the class variables and query to generate the menu filter
*
- * @return none but as a side-effect modify internal class variables
+ * @return void but as a side-effect modify internal class variables
*/
function _setupVariables()
{
@@ -127,7 +127,7 @@ class Mri_Protocol_Violations extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/mri_violations/php/mri_violations.class.inc b/modules/mri_violations/php/mri_violations.class.inc
index 3fd2df7a0b8..05bc2110cf6 100644
--- a/modules/mri_violations/php/mri_violations.class.inc
+++ b/modules/mri_violations/php/mri_violations.class.inc
@@ -43,9 +43,9 @@ class Mri_Violations extends \NDB_Menu_Filter_Form
/**
* Process function
*
- * @param string $values the value of values
+ * @param array $values the values being processed
*
- * @return boolean true if the user is permitted to see violated scans
+ * @return bool true if the user is permitted to see violated scans
*/
function _process($values)
{
@@ -154,7 +154,7 @@ class Mri_Violations extends \NDB_Menu_Filter_Form
* Set up the class and smarty variables to use for the menu filter to
* generate the proper query for the menu filter
*
- * @return none
+ * @return void
*/
function _setupVariables()
{
@@ -339,7 +339,6 @@ class Mri_Violations extends \NDB_Menu_Filter_Form
);
$this->EqualityFilters[] = 'v.Site';
- return true;
}
/**
@@ -347,7 +346,7 @@ class Mri_Violations extends \NDB_Menu_Filter_Form
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/mri_violations/php/resolved_violations.class.inc b/modules/mri_violations/php/resolved_violations.class.inc
index 2d5a73e14b7..ec3434bfccb 100644
--- a/modules/mri_violations/php/resolved_violations.class.inc
+++ b/modules/mri_violations/php/resolved_violations.class.inc
@@ -42,9 +42,9 @@ class Resolved_Violations extends \NDB_Menu_Filter_Form
/**
* Process function
*
- * @param string $values the value of values
+ * @param array $values the value of values
*
- * @return boolean true if the user is permitted to see violated scans
+ * @return bool true if the user is permitted to see violated scans
*/
function _process($values)
{
@@ -143,7 +143,7 @@ class Resolved_Violations extends \NDB_Menu_Filter_Form
* Set up the class and smarty variables to use for the menu filter to
* generate the proper query for the menu filter
*
- * @return none
+ * @return void
*/
function _setupVariables()
{
@@ -316,14 +316,15 @@ class Resolved_Violations extends \NDB_Menu_Filter_Form
'v.SeriesUID',
'v.Resolved',
);
- return true;
+ return;
}
+
/**
* Does the setup required for this page. By default, sets up elements
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
diff --git a/modules/new_profile/README.md b/modules/new_profile/README.md
new file mode 100644
index 00000000000..1558efd577f
--- /dev/null
+++ b/modules/new_profile/README.md
@@ -0,0 +1,46 @@
+# New Profile
+
+## Purpose
+
+The new_profile module is intended to allow users to create new
+candidates in a LORIS database.
+
+## Intended Users
+
+It is primarily used by data entry staff to create candidates for
+data entry.
+
+It may also be used by users uploading imaging data to ensure that
+a candidate exists before uploading the image.
+
+## Scope
+
+The new_profile module only creates candidates.
+
+NOT in scope:
+
+The new_profile module does not create timepoints, batteries, or
+anything else related to candidates, only the candidate itself.
+
+## Configurations
+
+- `useEDC`: This config setting determines whether to prompt for
+ the EDC of the candidate being created.
+- `useProject`: This setting determines whether to prompt for the
+ project of the candidate being created.
+- `StartYear`, `EndYear`, `AgeMin`, `AgeMax`: These are used to
+ determine whether the candidate's date of birth is within the
+ range of the study protocol.
+- `PSCID`: This is used to determine whether the PSCID for the new
+ candidate is autogenerated, prompted for, and what the format of
+ the PSCID that is valid for the study is.
+
+## Permissions
+
+The module requires the data_entry permission and at least one site
+for the user creating a candidate.
+
+## Interactions With LORIS
+
+Upon candidate creation, the module includes a link to the
+timepoint_list module for the newly created candidate.
diff --git a/modules/next_stage/README.md b/modules/next_stage/README.md
new file mode 100644
index 00000000000..67bfde2e4e7
--- /dev/null
+++ b/modules/next_stage/README.md
@@ -0,0 +1,52 @@
+# Next Stage
+
+## Purpose
+
+The `next_stage` module provides a method to start the "next stage"
+of a candidate session. The "Stage" of a session goes from Screening
+to Visit to Approval. The Screening and Visit stages may have
+different instruments administered. The `next_stage` module prompts
+for the date of the visit and uses the data it gathers to populate
+the test battery for the Visit stage.
+
+However, note that the Screening stage is mostly historic and poorly
+tested. Most LORIS studies prefer to have a separate "Screening"
+visit, rather than a screening stage for each visit. The result is
+that the `next_stage` module is mostly used simply to populate the
+test battery for a session.
+
+## Intended Users
+
+The `next_stage` module is used by data entry staff in order to
+start the Visit stage of an instrument.
+
+## Scope
+
+The `next_stage` module only moves the stage of a session from the
+Screening to Visit. The final Approval or Recycling Bin stage is
+reached by sending to DCC (or Recycling Bin) on the `instrument_list`
+page.
+
+## Permissions
+
+The `data_entry` permission is required. The user must also be at
+the same CenterID as the timepoint being accessed.
+
+Access is also denied if the timepoint isn't startable (ie. if it's
+already been started.)
+
+## Configurations
+
+The `test_battery` table must be set up with instruments to be added
+to the `Visit` stage which meets the criteria of the timepoint (ie
+age range, visit, center..) or no tests will be added. This must
+be done before the stage is started.
+
+## Interactions with LORIS
+
+The `next_stage` module links back to the `instrument_list` page
+after a stage is started.
+
+The `next_stage` module inserts the on-site test battery into the
+flag table. It does NOT insert survey instruments, which are inserted
+by the `survey_accounts` module.
diff --git a/modules/reliability/php/reliability.class.inc b/modules/reliability/php/reliability.class.inc
index f2bf052104c..764e24f785f 100644
--- a/modules/reliability/php/reliability.class.inc
+++ b/modules/reliability/php/reliability.class.inc
@@ -58,7 +58,6 @@ class Reliability extends \NDB_Menu_Filter
}
return false;
}
-//@codingStandardsIgnoreEnd
/**
* GetSiteID Load
*
@@ -78,9 +77,8 @@ class Reliability extends \NDB_Menu_Filter
/**
* _SetupVariables function
*
- * @return string
+ * @return void
*/
-//@codingStandardsIgnoreStart
function _setupVariables()
{
$user =& \User::singleton();
@@ -188,7 +186,7 @@ class Reliability extends \NDB_Menu_Filter
'ProjectID' => 'candidate.ProjectID',
// 'Lock_record' => 'session.Lock_record'
);
- return true;
+ return;
}
/**
@@ -722,7 +720,7 @@ class Reliability extends \NDB_Menu_Filter
/**
* Override toJSON to append a calculated field to the default JSON object
*
- * @return a json encoded string of the headers and data from this table
+ * @return string a json encoded string of the headers and data from this table
*/
function toJSON()
{
diff --git a/modules/server_processes_manager/README.md b/modules/server_processes_manager/README.md
new file mode 100644
index 00000000000..674350506f7
--- /dev/null
+++ b/modules/server_processes_manager/README.md
@@ -0,0 +1,64 @@
+# Server Processes Manager
+
+## Purpose
+
+The server processes manager module is used to get information about processes that were
+launched asynchronously by other LORIS modules.
+
+## Intended Users
+
+The LORIS system administrator(s) should be the only user(s) that should have access to
+this module.
+
+## Scope
+
+This module displays the information relevant to the asynchronous processes
+(currently running or not) launched by other modules. The properties displayed for
+each process are as follows:
+
+- The internal LORIS process ID for the process
+- Pid: the Unix process ID of the command that was launched asynchronously.
+- Type: type of process (only mri_upload is currently implemented).
+- Stdout file: full Unix path of the Output log file on the LORIS server
+(STDOUT output stream for the process).
+- Stderr file: full Unix path of the Error log file on the LORIS server
+(STDERR output stream for the process).
+- Exit code file: full Unix path of the file on the LORIS server that will
+contain the exit code of the process once it finishes.
+- Exit code: Unix exit code for the process. This column will be empty until
+the process terminates.
+- Userid: the LORIS username of the user that launched the asynchronous process.
+- Start time: time (on the LORIS server) at which the process was started.
+- End time: time (on the LORIS server) at which the process ended. Will be
+empty until the process finishes.
+- Exit text: summary text describing the process execution result. Will be
+empty until the process finishes.
+
+By default, all processes (active or not) are displayed on the server processes
+manager page but the page provides a selection filter that can be used to display
+only specific processes. Filtering can be performed based on the PID, type of
+process or user ID. Finally, note that upon termination, the temporary files used
+to store the processe's STDOUT, STDERR and exit code are deleted by default. For
+processes that were launched by the imaging uploader though, these files are deleted
+only if the process terminates successfully (i.e with an exit code equal to 0).
+
+NOT in scope:
+
+The page does not offer the possibility to suspend or stop an active process.
+
+## Permissions
+
+server_processes_manager
+ - This permission allows the user access to the server processes manager
+module and the ability to view the information for all processes.
+
+## Configurations
+
+There are no configuration settings associated to this module.
+
+## Interactions with LORIS
+
+The imaging uploader module is currently the only LORIS module capable of
+launching an asynchronous process, namely the execution of the MRI processing
+pipeline on a successfully uploaded scan archive. Consequently, the result table
+will only show these type of processes.
diff --git a/modules/server_processes_manager/php/defaultdatabaseprovider.class.inc b/modules/server_processes_manager/php/defaultdatabaseprovider.class.inc
index f991c258f3a..c910a1bea5d 100644
--- a/modules/server_processes_manager/php/defaultdatabaseprovider.class.inc
+++ b/modules/server_processes_manager/php/defaultdatabaseprovider.class.inc
@@ -28,7 +28,7 @@ class DefaultDatabaseProvider implements IDatabaseProvider
/**
* Gets access to the database through the Database::singleton() method.
*
- * @return Database an instance of the database.
+ * @return \Database an instance of the database.
* @throws \DatabaseException if connection to the database cannot be established
*/
public function getDatabase()
diff --git a/modules/server_processes_manager/php/idatabaseprovider.class.inc b/modules/server_processes_manager/php/idatabaseprovider.class.inc
index c8ffa91cc06..0cd5e8714ba 100644
--- a/modules/server_processes_manager/php/idatabaseprovider.class.inc
+++ b/modules/server_processes_manager/php/idatabaseprovider.class.inc
@@ -28,7 +28,7 @@ interface IDatabaseProvider
/**
* Gets a database object used to access the database.
*
- * @return a database object used to access the database.
+ * @return \Database object used to access the database.
*/
function getDatabase();
}
diff --git a/modules/server_processes_manager/php/server_processes_manager.class.inc b/modules/server_processes_manager/php/server_processes_manager.class.inc
index fe77a89c217..52a15824655 100644
--- a/modules/server_processes_manager/php/server_processes_manager.class.inc
+++ b/modules/server_processes_manager/php/server_processes_manager.class.inc
@@ -42,7 +42,7 @@ class Server_Processes_Manager extends \NDB_Menu_Filter
/**
* Sets up the filter and result table layout.
*
- * @return bool true if the set up is successful, false otherwise.
+ * @return void
*/
function _setupVariables()
{
@@ -81,7 +81,7 @@ class Server_Processes_Manager extends \NDB_Menu_Filter
'userid' => 'userid',
);
- return true;
+ return;
}
@@ -90,7 +90,7 @@ class Server_Processes_Manager extends \NDB_Menu_Filter
* that are common to every type of page. May be overridden by a specific
* page or specific page type.
*
- * @return none
+ * @return void
*/
function setup()
{
@@ -100,7 +100,7 @@ class Server_Processes_Manager extends \NDB_Menu_Filter
$this->addBasicText('type', 'Type:');
$this->addBasicText('userid', 'UserId:');
- return true;
+ return;
}
/**
* Gathers JS dependecies and merge them with the parent
diff --git a/modules/statistics/README.md b/modules/statistics/README.md
new file mode 100644
index 00000000000..76d962c67ce
--- /dev/null
+++ b/modules/statistics/README.md
@@ -0,0 +1,59 @@
+# Statistics
+
+## Purpose
+
+The statistics module provides a mechanism to provide summary
+descriptive statistics to users of a LORIS instance.
+
+## Intended Users
+
+The module is intended to be used by researchers or PIs looking for
+at-a-glance statistics about the state of their research project.
+
+## Scope
+
+The statistics module only provides descriptive statistics. Scientific
+analysis should be done outside of LORIS on data extracted from the
+`data_query` module using standard scientific tools.
+
+## Permissions
+
+The `data_entry` permission is required in order to access the
+statistics module.
+
+## Configurations
+
+The Statistics module includes a number of tabs for different types
+of statistics. These can be customized by populating the `StatisticsTabs`
+SQL table. Projects intending to customize the module should do so
+by creating a new project/modules module (ie `statistics_projectname`)
+which contain subpages for the tabs they'd like to use and populating
+the `StatisticsTabs` table, rather than modifying the module itself
+(this will make upgrading of LORIS easier).
+
+## Interactions with LORIS
+
+The default tabs provide summary of data from a variety of different
+LORIS modules. In particular, the data entry statistics gather data
+from instruments, and the imaging statistics use data from both the
+files and tarchive tables. The default tabs are described in further
+detail below.
+
+### Demographic Statistics
+
+This tab provides general statistics relating to the number of candidates
+registered in each cohort as well as customizable categories displaying
+statistics relating to candidate demographics. Statistics can be broken
+down by instrument by selecting an instrument from the dropdown menu.
+
+### Behavioural Statistics
+
+This tab provides data entry statistics relating to the number of
+candidates who have completed each instrument per site and timepoint.
+Statistics can be broken down by instrument or participant. DDE
+statistics are also provided.
+
+### MRI Statistics
+
+This tab displays the number of scans inserted per site as well as their
+QC status. Scan completion is arranged by site, cohort, and timepoint.
diff --git a/modules/statistics/php/module.class.inc b/modules/statistics/php/module.class.inc
index 81867df6cc2..b8f2f318c2c 100644
--- a/modules/statistics/php/module.class.inc
+++ b/modules/statistics/php/module.class.inc
@@ -3,7 +3,7 @@
* This serves as a hint to LORIS that this module is a real module.
* It does nothing but implement the module class in the module's namespace.
*
- * PHP Version 5
+ * PHP Version 7
*
* @category Behavioural
* @package Main
diff --git a/modules/statistics/php/statistics.class.inc b/modules/statistics/php/statistics.class.inc
index a1a2b5dfc71..243fb3ecb1f 100644
--- a/modules/statistics/php/statistics.class.inc
+++ b/modules/statistics/php/statistics.class.inc
@@ -1,8 +1,8 @@
query_vars
);
- return $result;
+ return $result;
}
/**
diff --git a/modules/statistics/php/statistics_mri_site.class.inc b/modules/statistics/php/statistics_mri_site.class.inc
index 3ae5731e846..99e96713308 100644
--- a/modules/statistics/php/statistics_mri_site.class.inc
+++ b/modules/statistics/php/statistics_mri_site.class.inc
@@ -1,8 +1,8 @@
hasCenterPermission('data_entry', $centerID);
}
@@ -121,7 +122,7 @@ class Statistics_Site extends \NDB_Form
* @param string $projectID the value of projectID
* @param string $instrument the value of instrument
*
- * @return void
+ * @return array
*/
function _getResults($centerID, $projectID, $instrument)
{
diff --git a/modules/statistics/php/stats_behavioural.class.inc b/modules/statistics/php/stats_behavioural.class.inc
index 7eb713ae863..7cc35e4e347 100644
--- a/modules/statistics/php/stats_behavioural.class.inc
+++ b/modules/statistics/php/stats_behavioural.class.inc
@@ -1,8 +1,8 @@
Per Instrument Stats
{foreach from=$Centers item=center key=centername}
- Please Click Here
+ View Details
{/foreach}
@@ -102,7 +102,7 @@