diff --git a/SQL/0000-00-01-Modules.sql b/SQL/0000-00-01-Modules.sql index f5fcc807b2d..82d25fa4c16 100644 --- a/SQL/0000-00-01-Modules.sql +++ b/SQL/0000-00-01-Modules.sql @@ -51,5 +51,6 @@ INSERT INTO modules (Name, Active) VALUES ('timepoint_list', 'Y'); INSERT INTO modules (Name, Active) VALUES ('user_accounts', 'Y'); INSERT INTO modules (Name, Active) VALUES ('electrophysiology_browser', 'Y'); INSERT INTO modules (Name, Active) VALUES ('dqt', 'Y'); +INSERT INTO modules (Name, Active) VALUES ('electrophysiology_uploader', 'Y'); ALTER TABLE issues ADD CONSTRAINT `fk_issues_7` FOREIGN KEY (`module`) REFERENCES `modules` (`ID`); diff --git a/SQL/0000-00-03-ConfigTables.sql b/SQL/0000-00-03-ConfigTables.sql index b09e2289a59..726eb552320 100644 --- a/SQL/0000-00-03-ConfigTables.sql +++ b/SQL/0000-00-03-ConfigTables.sql @@ -77,6 +77,8 @@ INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'MINCToolsPath', 'Path to the MINC tools', 1, 0, 'web_path', ID, 'Path to the MINC tools', 12 FROM ConfigSettings WHERE Name="paths"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'documentRepositoryPath', 'Path to uploaded document repository files', 1, 0, 'web_path', ID, 'Document Repository Upload Path', 13 FROM ConfigSettings WHERE Name="paths"; INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'dataReleasePath', 'Path to uploaded data release files', 1, 0, 'web_path', ID, 'Data release Upload Path', 14 FROM ConfigSettings WHERE Name="paths"; +INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'EEGUploadIncomingPath', 'Path to the upload directory for incoming EEG studies', 1, 0, 'text', ID, 'EEG Incoming Directory', 7 FROM ConfigSettings WHERE Name="paths"; + INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, Label, OrderNumber) VALUES ('gui', 'Settings related to the overall display of LORIS', 1, 0, 'GUI', 3); @@ -284,4 +286,4 @@ INSERT INTO Config (ConfigID, Value) SELECT ID, '' FROM ConfigSettings WHERE Na INSERT INTO Config (ConfigID, Value) SELECT ID, '' FROM ConfigSettings WHERE Name='bids_dataset_authors'; INSERT INTO Config (ConfigID, Value) SELECT ID, '' FROM ConfigSettings WHERE Name='bids_acknowledgments_text'; INSERT INTO Config (ConfigID, Value) SELECT ID, '' FROM ConfigSettings WHERE Name='bids_readme_text'; -INSERT INTO Config (ConfigID, Value) SELECT ID, '' FROM ConfigSettings WHERE Name='bids_validator_options_to_ignore'; +INSERT INTO Config (ConfigID, Value) SELECT ID, '' FROM ConfigSettings WHERE Name='bids_validator_options_to_ignore'; \ No newline at end of file diff --git a/SQL/0000-00-05-ElectrophysiologyTables.sql b/SQL/0000-00-05-ElectrophysiologyTables.sql index f15eba80e8a..f1923caf9b0 100644 --- a/SQL/0000-00-05-ElectrophysiologyTables.sql +++ b/SQL/0000-00-05-ElectrophysiologyTables.sql @@ -380,6 +380,21 @@ CREATE TABLE `physiological_annotation_rel` ( REFERENCES `physiological_annotation_file` (`AnnotationFileID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- Create EEG upload table +CREATE TABLE `electrophysiology_uploader` ( + `UploadID` int(10) unsigned NOT NULL AUTO_INCREMENT, + `UploadedBy` varchar(255) NOT NULL DEFAULT '', + `UploadDate` DateTime DEFAULT NULL, + `UploadLocation` varchar(255) NOT NULL DEFAULT '', + `Status` enum('Not Started', 'In Progress', 'Complete', 'Failed') DEFAULT 'Not Started', + `SessionID` int(10) unsigned NOT NULL default '0', + `MetaData` TEXT default NULL, + PRIMARY KEY (`UploadID`), + KEY (`SessionID`), + CONSTRAINT `FK_eegupload_SessionID` + FOREIGN KEY (`SessionID`) REFERENCES `session` (`ID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- Insert into physiological_output_type INSERT INTO physiological_output_type (`OutputTypeName`, `OutputTypeDescription`) diff --git a/SQL/New_patches/2023-02-24-electrophysiology_uploader.sql b/SQL/New_patches/2023-02-24-electrophysiology_uploader.sql new file mode 100644 index 00000000000..363482be29f --- /dev/null +++ b/SQL/New_patches/2023-02-24-electrophysiology_uploader.sql @@ -0,0 +1,21 @@ +-- Create EEG upload table +CREATE TABLE `electrophysiology_uploader` ( + `UploadID` int(10) unsigned NOT NULL AUTO_INCREMENT, + `UploadedBy` varchar(255) NOT NULL DEFAULT '', + `UploadDate` DateTime DEFAULT NULL, + `UploadLocation` varchar(255) NOT NULL DEFAULT '', + `Status` enum('Not Started', 'In Progress', 'Complete', 'Failed') DEFAULT 'Not Started', + `SessionID` int(10) unsigned NOT NULL default '0', + `MetaData` TEXT default NULL, + PRIMARY KEY (`UploadID`), + KEY (`SessionID`), + CONSTRAINT `FK_eegupload_SessionID` + FOREIGN KEY (`SessionID`) REFERENCES `session` (`ID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Add to module table +INSERT INTO modules (Name, Active) VALUES ('electrophysiology_uploader', 'Y'); + +-- Add new configurations for eeg uploader +INSERT INTO ConfigSettings (Name, Description, Visible, AllowMultiple, DataType, Parent, Label, OrderNumber) SELECT 'EEGUploadIncomingPath', 'Path to the upload directory for incoming EEG studies', 1, 0, 'text', ID, 'EEG Incoming Directory', 7 FROM ConfigSettings WHERE Name="paths"; + diff --git a/modules/electrophysiology_uploader/json/ConversionFlags.json b/modules/electrophysiology_uploader/json/ConversionFlags.json new file mode 100644 index 00000000000..4e125d13cea --- /dev/null +++ b/modules/electrophysiology_uploader/json/ConversionFlags.json @@ -0,0 +1,359 @@ +{ + "FACE_present": { + "pass": "There are face stimuli flags", + "warning": "No face flags! There might be connection issue between E-prime and Netstation computers. Be sure to open netstation BEFORE E-prime, and check that the stm+ and fix+ flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "FACE Flags Are Present", + "reason": false + }, + "FACE_num": { + "pass": "The number if face stimuli flags is correct.", + "warning": "Missing Face Flag! This might mean the task was quit early. Please explain what happened:", + "flagCondition": 0, + "label": "Correct Number Of Face Flags", + "reason": true + }, + "face_present": { + "pass": "There are face stimuli flags", + "warning": "No face flags! There might be connection issue between E-prime and Netstation computers. Be sure to open netstation BEFORE E-prime, and check that the stm+ and fix+ flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "FACE Flags Are Present", + "reason": false + }, + "face_num": { + "pass": "The number if face stimuli flags is correct.", + "warning": "Missing Face Flag! This might mean the task was quit early. Please explain what happened:", + "flagCondition": 0, + "label": "Correct Number Of Face Flags", + "reason": true + }, + "VEP_present": { + "pass": "There are VEP stimuli flags", + "warning": "Noe VEP flags! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE Eprime and check that the ch1+ and ch2+ flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "VEP Flags Are Present", + "reason": false + }, + "VEP_num": { + "pass": "The number of VEP flags is correct", + "warning": "Missing VEP Flag! This might mean the task was quit early. Please explain what happened:", + "flagCondition": 0, + "label": "Correct Number Of VEP Flags", + "reason": true + }, + "MMN_present": { + "pass": "There are MMN stimuli flags", + "warning": "No MMN flags! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE Eprime, and check that the ch1+ and ch2+ flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "MMN Flags Are Present", + "reason": false + }, + "MMN_num": { + "pass": "The number of MMN flags is correct (equal to either exactly 1 or exactly 2 blocks)", + "warning": "Missing MMN Flag! This might mean the task was quit early. Please explain what happened:", + "flagCondition": 0, + "label": "Correct Number Of MMN Flags", + "reason": true + }, + "RS_present": { + "pass": "There is a Resting Sqtate stimuli flag", + "warning": "No Resting State flags! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE E-prime, and check that the bas+ flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "RS Flags Are Present", + "reason": false + }, + "trsp_RS_present": { + "pass": "There is at least one TRSP flag in RS task", + "warning": "No TRSP flags in RS! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE E-prime, and check that the TRSP flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "TRSP Flags Are Present for RS", + "reason": false + }, + "trsp_RS_num": { + "pass": "The number of TRSP flags is correct in RS", + "warning": "Missing TRSP Flag in RS! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of TRSP Flags for RS", + "reason": false + }, + "bgin_RS_present": { + "pass": "There is at least one bgin flag in RS", + "warning": "No bgin flags in RS! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE Eprime, and check that the bgin flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "bgin Flags Are Present for RS", + "reason": false + }, + "bgin_RS_num": { + "pass": "The number of bgin flags is correct in RS", + "warning": "Missing bgin Flag in RS! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of bgin Flags for RS", + "reason": false + }, + "delay_RS": { + "pass": "There is a flag delay in RS", + "warning": "Possible flag delay in RS! Make sure Netstation is running before opening E-prime and check that the SESS and CELL flags appear in netstation right as the recording is started. If the problem persists, restart both computers.", + "flagCondition": 1, + "label": "Delayed Flags for RS", + "reason": false + }, + "IBEG_RS": { + "pass": "Impedances were opened during the task in RS.", + "warning": "The impedances were opened during the RS. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Left Open for RS", + "reason": false + }, + "IEND_RS": { + "pass": "Impedances were closed during the RS task.", + "warning": "The impedances were closed during RS. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Closed During RS", + "reason": false + }, + "SESS_RS_present": { + "pass": "The SESS flag exists in RS", + "warning": "Missing SESS flag in RS! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "SESS Flags Are Present for RS", + "reason": false + }, + "CELL_RS_present": { + "pass": "The CELL flag exists in RS", + "warning": "Missing CELL flag in RS! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "CELL Flags Are Present for RS", + "reason": false + }, + "RS_exists": { + "pass": "RS task exists.", + "warning": "RS is missing!", + "flagCondition": 0, + "label": "RS Was Uploaded", + "reason": false + }, + "trsp_MMN_present": { + "pass": "There is at least one TRSP flag in MMN", + "warning": "No TRSP flags in MMN! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE E-prime, and check that the TRSP flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "TRSP Flags Are Present for MMN", + "reason": false + }, + "trsp_MMN_num": { + "pass": "The number of TRSP flags is correct", + "warning": "Missing TRSP Flag in *TASK*! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of TRSP Flags for MMN", + "reason": false + }, + "bgin_MMN_present": { + "pass": "There is at least one bgin flag in MMN", + "warning": "No bgin flags in MMN! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE Eprime, and check that the bgin flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "bgin Flags Are Present for MMN", + "reason": false + }, + "bgin_MMN_num": { + "pass": "The number of bgin flags is correct in MMN", + "warning": "Missing bgin Flag in MMN! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of bgin Flags for MMN", + "reason": false + }, + "delay_MMN": { + "pass": "There is a flag delay in MMN", + "warning": "Possible flag delay in MMN! Make sure Netstation is running before opening E-prime and check that the SESS and CELL flags appear in netstation right as the recording is started. If the problem persists, restart both computers.", + "flagCondition": 1, + "label": "Delayed Flags for MMN", + "reason": false + }, + "IBEG_MMN": { + "pass": "Impedances were opened during the MMN.", + "warning": "The impedances were opened during the MMN. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Left Open for MMN", + "reason": false + }, + "IEND_MMN": { + "pass": "Impedances were closed during the MMN.", + "warning": "The impedances were closed during MMN. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Closed During MMN", + "reason": false + }, + "SESS_MMN_present": { + "pass": "The SESS flag exists in MMN", + "warning": "Missing SESS flag in MMN! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "SESS Flags Are Present for MMN", + "reason": false + }, + "CELL_MMN_present": { + "pass": "The CELL flag exists in MMN", + "warning": "Missing CELL flag in MMN! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "CELL Flags Are Present for MMN", + "reason": false + }, + "MMN_exists": { + "pass": "This task exists.", + "warning": "MMN is missing!", + "flagCondition": 0, + "label": "MMN Was Uploaded", + "reason": false + }, + "trsp_FACE_present": { + "pass": "There is at least one TRSP flag in FACE", + "warning": "No TRSP flags in FACE! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE E-prime, and check that the TRSP flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "TRSP Flags Are Present for FACE", + "reason": false + }, + "trsp_FACE_num": { + "pass": "The number of TRSP flags is correct in FACE", + "warning": "Missing TRSP Flag in FACE! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of TRSP Flags for FACE", + "reason": false + }, + "bgin_FACE_present": { + "pass": "There is at least one bgin flag in FACE", + "warning": "No bgin flags in FACE! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE Eprime, and check that the bgin flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "bgin Flags Are Present for FACE", + "reason": false + }, + "bgin_FACE_num": { + "pass": "The number of bgin flags is correct in FACE", + "warning": "Missing bgin Flag in FACE! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of bgin Flags for FACE", + "reason": false + }, + "delay_FACE": { + "pass": "There is a flag delay in FACE", + "warning": "Possible flag delay in FACE! Make sure Netstation is running before opening E-prime and check that the SESS and CELL flags appear in netstation right as the recording is started. If the problem persists, restart both computers.", + "flagCondition": 1, + "label": "Delayed Flags for FACE", + "reason": false + }, + "IBEG_FACE": { + "pass": "Impedances were opened during the FACE.", + "warning": "The impedances were opened during the FACE. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Left Open for FACE", + "reason": false + }, + "IEND_FACE": { + "pass": "Impedances were closed during the FACE.", + "warning": "The impedances were closed during FACE. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Closed During FACE", + "reason": false + }, + "SESS_FACE_present": { + "pass": "The SESS flag exists in FACE", + "warning": "Missing SESS flag in FACE! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "SESS Flags Are Present for FACE", + "reason": false + }, + "CELL_FACE_present": { + "pass": "The CELL flag exists in FACE", + "warning": "Missing CELL flag in FACE! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "CELL Flags Are Present for FACE", + "reason": false + }, + "FACE_exists": { + "pass": "FACE task exists.", + "warning": "FACE is missing!", + "flagCondition": 0, + "label": "FACE Was Uploaded", + "reason": false + }, + "trsp_VEP_present": { + "pass": "There is at least one TRSP flag in VEP", + "warning": "No TRSP flags in VEP! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE E-prime, and check that the TRSP flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "TRSP Flags Are Present for VEP", + "reason": false + }, + "trsp_VEP_num": { + "pass": "The number of TRSP flags is correct in VEP", + "warning": "Missing TRSP Flag in VEP! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of TRSP Flags for VEP", + "reason": false + }, + "bgin_VEP_present": { + "pass": "There is at least one bgin flag in VEP", + "warning": "No bgin flags in VEP! There might be a connection issue between the Eprime and Netstation computers. Be sure to open netstation BEFORE Eprime, and check that the bgin flags are showing up in Netstation while the task is running.", + "flagCondition": 0, + "label": "bgin Flags Are Present for VEP", + "reason": false + }, + "bgin_VEP_num": { + "pass": "The number of bgin flags is correct in VEP", + "warning": "Missing bgin Flag in VEP! This might mean the task was quit early.", + "flagCondition": 0, + "label": "Correct Number Of bgin Flags for VEP", + "reason": false + }, + "delay_VEP": { + "pass": "There is a flag delay in VEP", + "warning": "Possible flag delay in VEP! Make sure Netstation is running before opening E-prime and check that the SESS and CELL flags appear in netstation right as the recording is started. If the problem persists, restart both computers.", + "flagCondition": 1, + "label": "Delayed Flags for VEP", + "reason": false + }, + "IBEG_VEP": { + "pass": "Impedances were opened during the VEP.", + "warning": "The impedances were opened during the VEP. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Left Open for VEP", + "reason": false + }, + "IEND_VEP": { + "pass": "Impedances were closed during the VEP.", + "warning": "The impedances were closed during VEP. Make sure you close the impedance window before beginning a task.", + "flagCondition": 1, + "label": "Impedances Closed During VEP", + "reason": false + }, + "SESS_VEP_present": { + "pass": "The SESS flag exists in VEP", + "warning": "Missing SESS flag in VEP! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "SESS Flags Are Present for VEP", + "reason": false + }, + "CELL_VEP_present": { + "pass": "The CELL flag exists in VEP", + "warning": "Missing CELL flag in VEP! This probably isn’t an issue, but make sure Netstation is turned on before Eprime.", + "flagCondition": 0, + "label": "CELL Flags Are Present for VEP", + "reason": false + }, + "VEP_exists": { + "pass": "VEP task exists.", + "warning": "VEP is missing!", + "flagCondition": 0, + "label": "VEP Was Uploaded", + "reason": false + }, + "duplicate_file": { + "pass": "There aren\"t multiple files for the same task.", + "warning": "There are multiple files of the same task. Please explain why:", + "flagCondition": 1, + "label": "Multiple Runs of the Same Task", + "reason": true + }, + "high_impedance": { + "pass": "No electrodes had high impedances.", + "warning": "One or more electrodes had high impedances! Impedances can be improved by making sure all electrodes (especially ref and com) have good contact with the scalp, and the sponges are wet. If the same electrode is persistently bad, it might need to be replaced.", + "flagCondition": 1, + "label": "High Impedances", + "reason": false + } +} diff --git a/modules/electrophysiology_uploader/jsx/ElectrophysiologyUploader.js b/modules/electrophysiology_uploader/jsx/ElectrophysiologyUploader.js new file mode 100644 index 00000000000..4a5a94321ec --- /dev/null +++ b/modules/electrophysiology_uploader/jsx/ElectrophysiologyUploader.js @@ -0,0 +1,97 @@ +import {Component} from 'react'; +import Loader from '../../../../jsx/Loader'; +import {TabPane, Tabs} from '../../../../jsx/Tabs'; +import UploadForm from './UploadForm'; +import UploadViewer from './UploadViewer'; + +class ElectrophysiologyUploader extends Component { + constructor(props) { + super(props); + + this.state = { + isLoaded: false, + filter: {}, + fieldOptions: {}, + }; + + this.fetchData = this.fetchData.bind(this); + } + + /** + * Called by React when the component has been rendered on the page. + */ + componentDidMount() { + this.fetchData(); + } + + /** + * Retrive data from the provided URL and save it in state + * Additionaly add hiddenHeaders to global loris vairable + * for easy access by columnFormatter. + */ + fetchData() { + fetch(`${this.props.DataURL}/?format=json`, { + method: 'GET', + }).then((response) => { + if (!response.ok) { + console.error(response.status + ': ' + response.statusText); + return; + } + + response.json().then((data) => { + this.setState({ + data: data, + isLoaded: true, + }); + }); + }).catch((error) => { + console.error(error); + }); + } + + render() { + if (!this.state.isLoaded) { + return ; + } + + const tabList = [ + {id: 'browse', label: 'Browse'}, + {id: 'upload', label: 'Upload'}, + ]; + + return ( + + + + + + + + + ); + } +} + +/** + * Render imaging_uploader on page load + */ +document.addEventListener('DOMContentLoaded', function() { + const electrophysiologyUploader = ( +
+ +
+ ); + + const root = ReactDOM.createRoot( + document.getElementById('lorisworkspace') + ); + root.render(electrophysiologyUploader); +}); diff --git a/modules/electrophysiology_uploader/jsx/UploadForm.js b/modules/electrophysiology_uploader/jsx/UploadForm.js new file mode 100644 index 00000000000..b437b462ea6 --- /dev/null +++ b/modules/electrophysiology_uploader/jsx/UploadForm.js @@ -0,0 +1,363 @@ +import React, {Component} from 'react'; + +import ProgressBar from 'ProgressBar'; +import swal from 'sweetalert2'; + +/** + * Imaging Upload Form + * Form component allowing to upload MRI images to LORIS + * + * @author Alex Ilea + * @author Victoria Foing + * @version 1.0.0 + * @since 2017/04/01 + */ +class UploadForm extends Component { + /** + * @constructor + * @param {object} props - React Component properties + */ + constructor(props) { + super(props); + + this.state = { + formData: {}, + form: {}, + hasError: {}, + errorMessage: {}, + uploadProgress: -1, + }; + + this.onFormChange = this.onFormChange.bind(this); + this.submitForm = this.submitForm.bind(this); + this.uploadFile = this.uploadFile.bind(this); + } + + /** + * Updates values in formData + * + * @param {string} field + * @param {*} value + */ + onFormChange(field, value) { + if (!field) return; + + const form = JSON.parse(JSON.stringify(this.state.form)); + const formData = Object.assign({}, this.state.formData); + + formData[field] = value; + + if (field === 'eegFile' && value.name) { + let patientName = value.name.replace(/\.[a-z]+\.?[a-z]+?$/i, ''); + let ids = patientName.split('_'); + formData.pscid = ids[0]; + formData.candID = ids[1]; + formData.visit = ids[2]; + formData.metaData = JSON.stringify({}); + } + + this.setState({ + form: form, + formData: formData, + }); + } + + /** + * Submit form + */ + submitForm() { + // Validate required fields + const data = this.state.formData; + + if (!data.eegFile) { + return; + } + + const fileName = data.eegFile.name; + // Make sure file is of type .tar.gz format + const properExt = new RegExp('\.(tar.gz)$'); + const pNameElements = [data.pscid, data.candID, data.visit, 'bids']; + const fileNameConvention = pNameElements.join('_'); + if (!fileName.match(properExt)) { + swal.fire({ + title: 'Invalid extension for the uploaded file!', + text: 'Filename extension does not match .tar.gz ', + type: 'error', + confirmButtonText: 'OK', + }); + + let errorMessage = { + eegFile: 'The file ' + fileName + ' must be of type .tar.gz.', + candID: undefined, + pscid: undefined, + visit: undefined, + }; + + let hasError = { + eegFile: true, + candID: false, + pscid: false, + visit: false, + }; + + this.setState({errorMessage, hasError}); + return; + } + if (!fileName.match(fileNameConvention)) { + swal.fire({ + title: 'Invalid filename!', + text: 'Filename should be of the form ' + + '[PSCID]_[CandID]_[VisitLabel]_bids', + type: 'error', + confirmButtonText: 'OK', + }); + + let errorMessage = { + eegFile: 'The file ' + fileName + ' does not respect name convention ', + candID: undefined, + pscid: undefined, + visit: undefined, + }; + + let hasError = { + eegFile: true, + candID: false, + pscid: false, + visit: false, + }; + + this.setState({errorMessage, hasError}); + return; + } + + this.uploadFile(); + + return; + } + + /** + * Uploads file to the server, listening to the progress + * in order to get the percentage uploaded as value for the progress bar + * + * @param {boolean} overwriteFile + */ + uploadFile(overwriteFile) { + const formData = this.state.formData; + let formObj = new FormData(); + for (let key in formData) { + if (formData[key] !== '') { + formObj.append(key, formData[key]); + } + } + formObj.append('fire_away', 'Upload'); + if (overwriteFile) { + formObj.append('overwrite', true); + } + + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener('progress', (evt) => { + if (evt.lengthComputable) { + const percentage = Math.round((evt.loaded / evt.total) * 100); + this.setState({uploadProgress: percentage}); + } + }, false); + + xhr.addEventListener('load', () => { + if (xhr.status < 400) { + // Upon successful upload: + // - Resets errorMessage and hasError so no errors are displayed on form + // - Displays pop up window with success message + // - Returns to Browse tab + const errorMessage = this.state.errorMessage; + const hasError = this.state.hasError; + for (let i in errorMessage) { + if (errorMessage.hasOwnProperty(i)) { + errorMessage[i] = ''; + hasError[i] = false; + } + } + this.setState({errorMessage: errorMessage, hasError: hasError}); + let text = ''; + if (this.props.imagingUploaderAutoLaunch === 'true' || + this.props.imagingUploaderAutoLaunch === '1' + ) { + text = 'Processing of this file by the MRI pipeline has started\n' + + 'Select this upload in the result table ' + + 'to view the processing progress'; + } + swal.fire({ + title: 'Upload Successful!', + text: text, + type: 'success', + }); + } else { + this.processError(xhr); + } + }, false); + + xhr.addEventListener('error', () => { + this.processError(xhr); + }, false); + + xhr.open('POST', this.props.uploadURL); + xhr.send(formObj); + } + + /** + * Process XMLHttpRequest errors + * + * @param {XMLHttpRequest} xhr - XMLHttpRequest + */ + processError(xhr) { + // Upon errors in upload: + // - Displays pop up window with submission error message + // - Updates errorMessage and hasError so relevant errors are displayed on form + // - Returns to Upload tab + + console.error(xhr.status + ': ' + xhr.statusText); + + let errorMessage = Object.assign({}, this.state.errorMessage); + const hasError = Object.assign({}, this.state.hasError); + let messageToPrint = ''; + if (xhr.response) { + const resp = JSON.parse(xhr.response); + if (resp.errors) { + errorMessage = resp.errors; + } + } else if (xhr.status == 0) { + errorMessage = { + 'eegFile': ['Upload failed: a network error occured'], + }; + } else if (xhr.status == 413) { + errorMessage = { + 'eegFile': [ + 'Please make sure files are not larger than ' + + this.props.maxUploadSize, + ], + }; + } else { + errorMessage = { + 'eegFile': [ + 'Upload failed: received HTTP response code ' + + xhr.status, + ], + }; + } + for (const [key, error] of Object.entries(errorMessage)) { + errorMessage[key] = error.toString(); + if (error.length) { + hasError[key] = true; + messageToPrint += error + '\n'; + } else { + hasError[key] = false; + } + } + swal.fire({ + title: 'Submission error!', + text: messageToPrint, + type: 'error', + }); + this.setState({ + uploadProgress: -1, + errorMessage: errorMessage, + hasError: hasError, + }); + } + + /** + * Renders the React component. + * + * @return {JSX} - React markup for the component + */ + render() { + // Hide button when progress bar is shown + const btnClass = ( + (this.state.uploadProgress > -1) ? 'btn btn-primary hide' : undefined + ); + + const notes = ( + + File cannot exceed {this.props.maxUploadSize}
+ File must be of type .tar.gz
+ File name must match the following convention: + [PSCID]_[CandID]_[Visit Label]_bids.tar.gz
+ For example, for CandID 100000, PSCID ABC123, and + Visit Label V1 the file name should be: + ABC123_100000_V1_bids.tar.gz
+
+ ); + + // Returns individual form elements + // For CandID, PSCID, and Visit Label, disabled and required + // are updated depending on Phantom Scan value + // For all elements, hasError and errorMessage + // are updated depending on what values are submitted + return ( +
+
+

Upload an electrophysiology recording session

+
+ + + + + + +
+
+ +
+
+ +
+
+
+ ); + } +} + +UploadForm.propTypes = {}; +UploadForm.defaultProps = {}; + +export default UploadForm; diff --git a/modules/electrophysiology_uploader/jsx/UploadViewer.js b/modules/electrophysiology_uploader/jsx/UploadViewer.js new file mode 100644 index 00000000000..14b730a0936 --- /dev/null +++ b/modules/electrophysiology_uploader/jsx/UploadViewer.js @@ -0,0 +1,256 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import FilterableDataTable from '../../../../jsx/FilterableDataTable'; +import Modal from '../../../../jsx/Modal'; +import ConversionFlags from '../json/ConversionFlags'; + +/** + * UploadViewer + */ +class UploadViewer extends Component { + /** + * @constructor + * @param {object} props - React Component properties + */ + constructor(props) { + super(props); + + this.state = { + modelMode: '', + viewData: {}, + }; + + this.viewMoreData = this.viewMoreData.bind(this); + this.formatColumn = this.formatColumn.bind(this); + } + + /** + * viewMoreData + * + * @param {string} mode the type of data to view + * @param {object} data the data to view + */ + viewMoreData(mode, data) { + this.setState({ + modelMode: mode, + viewData: data, + }); + } + + /** + * Modify behaviour of specified column cells in the Data Table component + * + * @param {string} column - column name + * @param {string} cell - cell content + * @param {object} row - row content indexed by column + * @return {*} a formated table cell for a given column + */ + formatColumn(column, cell, row) { + switch (column) { + case 'Upload Location': + const downloadURL = + loris.BaseURL + + '/electrophysiology_uploader/upload?' + + `upload_id=${row['Upload ID']}`; + return ( + + + {cell} + + + ); + case 'Completed Tasks': + let excluded = {}; + let count = 0; + try { + excluded = cell ? JSON.parse(cell) : {}; + count = excluded ? Object.keys(excluded).length : 0; + } catch (error) { + console.warn(error); + } + const tasksData = { + pscid: row['PSCID'], + vist: row['Visit'], + excluded: excluded, + }; + return ( + + this.viewMoreData('exclude', tasksData)} + >{4-count} of 4 + + ); + case 'Raised Flags Details': + const raisedFlags = []; + let flagsData = {}; + + try { + row['Raised Flags'].forEach((flag) => { + const raisedFlag = { + flag: flag, + }; + if (ConversionFlags[flag].reason) { + raisedFlag.reason = cell.reasons[flag]; + } + raisedFlags.push(raisedFlag); + }); + + flagsData = { + pscid: row['PSCID'], + vist: row['Visit'], + flags: raisedFlags, + additional: cell?.reasons?.additional, + }; + } catch (error) { + console.warn(error); + } + + return ( + + this.viewMoreData('flags', flagsData)} + >{raisedFlags.length} + + ); + case 'Raised Flags': + return ( + {cell?.join(', ')} + ); + default: + return {cell}; + } + } + + /** + * Renders the React component. + * + * @return {JSX} - React markup for the component + */ + render() { + const { + modelMode, + viewData, + } = this.state; + + const flagOpts = {}; + Object.keys(ConversionFlags).map(function(key) { + flagOpts[key] = ConversionFlags[key].label; + }); + + const fields = [ + { + label: 'Upload ID', + show: true, + }, + { + label: 'Site', + show: true, + filter: { + name: 'site', + type: 'multiselect', + options: this.props.fieldOptions.sites, + }, + }, + { + label: 'PSCID', + show: true, + }, + { + label: 'Visit', + show: true, + }, + { + label: 'Upload Location', + show: true, + }, + { + label: 'Upload Time', + show: false, + }, + { + label: 'Status', + show: true, + }, + { + label: 'Uploaded By', + show: true, + }, + { + label: 'Completed Tasks', + show: true, + }, + { + label: 'Raised Flags Details', + show: true, + }, + { + label: 'Raised Flags', + show: false, + filter: { + name: 'flags', + type: 'multiselect', + options: flagOpts, + }, + }, + ]; + + const viewTitle = modelMode === 'exclude' + ? 'Excluded Tasks' + : 'Raised Flags'; + let title = viewData === {} + ? '' + : `${viewTitle} for ${viewData.pscid}/${viewData.vist}: `; + + return ( + <> + + this.viewMoreData('', {})} + > + {modelMode === 'exclude' && ( +
    + {Object.keys(viewData.excluded).map((task) => ( +
    +
    + {task}: + {viewData.excluded[task]} +
    +
    + ))} +
+ )} + {modelMode === 'flags' && ( +
    + {viewData?.flags?.map((flag) => ( +
  • + {ConversionFlags[flag.flag].warning} + {flag.reason ? ': ' + flag.reason : ''} +
  • + ))} + {viewData.additional ? ( +
  • + Additional Comments: + {viewData.additional} +
  • + ) : ''} +
+ )} +
+ + ); + } +} + +UploadViewer.propTypes = { + data: PropTypes.array.isRequired, + fieldOptions: PropTypes.object.isRequired, +}; + +export default UploadViewer; diff --git a/modules/electrophysiology_uploader/php/electrophysiology_uploader.class.inc b/modules/electrophysiology_uploader/php/electrophysiology_uploader.class.inc new file mode 100644 index 00000000000..c8a8c25a6cc --- /dev/null +++ b/modules/electrophysiology_uploader/php/electrophysiology_uploader.class.inc @@ -0,0 +1,95 @@ +hasPermission('electrophysiology_browser_view_allsites') + || ($user->hasPermission('electrophysiology_browser_view_site') + && $user->hasStudySite() + ) + ); + } + + /** + * Tells the base class that this page's provisioner can support the + * HasAnyPermissionOrUserSiteMatch filter. + * + * @return ?array of site permissions or null + */ + public function allSitePermissionNames() : ?array + { + return ['electrophysiology_browser_view_allsites']; + } + + /** + * @inheritDoc + */ + public function useProjectFilter(): bool + { + return false; + } + + /** + * @inheritDoc + */ + protected function getFieldOptions(): array + { + // create user object + $factory = \NDB_Factory::singleton(); + $user = $factory->user(); + + if ($user->hasPermission('access_all_profiles')) { + $list_of_sites = \Utility::getSiteList(); + } else { + $list_of_sites = $user->getStudySites(); + } + + $site_options = []; + foreach (array_values($list_of_sites) as $name) { + $site_options[$name] = $name; + } + + return [ + 'sites' => $site_options + ]; + } + + /** + * @inheritDoc + */ + public function getBaseDataProvisioner(): \LORIS\Data\Provisioner + { + return new ElectrophysiologyUploaderProvisioner(); + } + + /** + * {@inheritDoc} + * + * @return array of javascript to be inserted + */ + function getJSDependencies() : array + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + $deps = parent::getJSDependencies(); + return array_merge( + $deps, + [ + $baseURL . "/electrophysiology_uploader/js/ElectrophysiologyUploader.js", + ] + ); + } +} diff --git a/modules/electrophysiology_uploader/php/electrophysiologyuploaderprovisioner.class.inc b/modules/electrophysiology_uploader/php/electrophysiologyuploaderprovisioner.class.inc new file mode 100644 index 00000000000..3e8fbfbbce9 --- /dev/null +++ b/modules/electrophysiology_uploader/php/electrophysiologyuploaderprovisioner.class.inc @@ -0,0 +1,71 @@ + $metaData->flags, + 'reasons' => $metaData->reasons + ]; + + $row['excluded'] = $metaData->exclude; + $row['flags'] = $flags; + $row['raisedFlags'] = array_values( + array_filter( + array_keys((array) $metaData->flags), + function ($flag) use ($metaData, $flagsData) { + return $metaData->flags->$flag === $flagsData[$flag]['flagCondition']; + } + ) + ); + + return new ElectrophysiologyUploaderRow($row, $cid, $pid); + } +} diff --git a/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc b/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc new file mode 100644 index 00000000000..0287e774e4c --- /dev/null +++ b/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc @@ -0,0 +1,58 @@ +DBRow = $row; + $this->CenterID = $cid; + $this->ProjectID = $pid; + } + + /** + * Implements \LORIS\Data\DataInstance interface for this row. + * + * @return array which can be serialized by json_encode() + */ + public function jsonSerialize() : array + { + return $this->DBRow; + } + + /** + * Returns the CenterID for this row, for filters such as + * \LORIS\Data\Filters\UserSiteMatch to match again. + * + * @return \CenterID + */ + public function getCenterID(): \CenterID + { + return $this->CenterID; + } + + /** + * Returns the ProjectID for this row + * + * @return \ProjectID The ProjectID + */ + public function getProjectID(): \ProjectID + { + return $this->ProjectID; + } +} diff --git a/modules/electrophysiology_uploader/php/module.class.inc b/modules/electrophysiology_uploader/php/module.class.inc new file mode 100644 index 00000000000..3b65da7d2f7 --- /dev/null +++ b/modules/electrophysiology_uploader/php/module.class.inc @@ -0,0 +1,65 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace LORIS\electrophysiology_uploader; +/** + * Class module implements the basic LORIS module functionality + * + * @category Behavioural + * @package Main + * @subpackage Electophysiology + * @author Cécile Madjar + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Module extends \Module +{ + /** + * {@inheritDoc} + * + * @param \User $user The user whose access is being checked. + * + * @return bool whether access is granted + */ + public function hasAccess(\User $user) : bool + { + return parent::hasAccess($user) && + $user->hasAnyPermission( + [ + 'electrophysiology_browser_view_allsites', + 'electrophysiology_browser_view_site' + ] + ); + } + + /** + * {@inheritDoc} + * + * @return string The menu category for this module + */ + public function getMenuCategory() : string + { + return "Electrophysiology"; + } + + /** + * {@inheritDoc} + * + * @return string The human readable name for this module + */ + public function getLongName() : string + { + return "Electrophysiology Uploader"; + } +} diff --git a/modules/electrophysiology_uploader/php/upload.class.inc b/modules/electrophysiology_uploader/php/upload.class.inc new file mode 100644 index 00000000000..c9f2ab6cd01 --- /dev/null +++ b/modules/electrophysiology_uploader/php/upload.class.inc @@ -0,0 +1,229 @@ +getMethod()) { + case 'GET': + return $this->_handleGet($request); + case 'POST': + return $this->_handlePOST($request); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + /** + * Handle an incoming HTTP GET request to download uploaded file. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface + */ + private function _handleGet(ServerRequestInterface $request) : ResponseInterface + { + $qParams = $request->getQueryParams(); + if (!isset($qParams['upload_id'])) { + return new \LORIS\Http\Response\JSON\BadRequest( + "upload_id query parameter is required" + ); + } + + $db = $this->loris->getDatabaseConnection(); + $file = $db->pselectOne( + " + SELECT UploadLocation + FROM electrophysiology_uploader + WHERE UploadID = :uploadID + ", + ['uploadID' => $qParams['upload_id']] + ); + + if (empty($file)) { + return new \LORIS\Http\Response\JSON\NotFound( + "File not found." + ); + } + + $filename = urldecode(basename($file)); + $pos = strrpos($file, '/'); + $file_rel_path = $pos === false ? '' : substr($file, 0, $pos); + $config = \NDB_Config::singleton(); + $upload_dir = new \SplFileInfo($config->getSetting('EEGUploadIncomingPath')); + + $downloader = new \LORIS\FilesDownloadHandler( + new \SPLFileInfo($upload_dir . '/' . $file_rel_path) + ); + + return $downloader->handle( + $request->withAttribute('filename', $filename) + ); + } + + /** + * Processes the values & saves to database and return a json response. + * + * @param ServerRequestInterface $request The incoming PSR7 request. + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handlePOST(ServerRequestInterface $request) : ResponseInterface + { + $db = $this->loris->getDatabaseConnection(); + + $uploadedFile = $request->getUploadedFiles()['eegFile'] ?? null; + $values = $request->getParsedBody(); + $pscid = $values['pscid']; + $candID = $values['candID']; + $visitLabel = $values['visit']; + + if (is_null($uploadedFile)) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'No file uploaded' + ); + } + + $sessionID = $db->pselectOne( + " + SELECT s.ID + FROM session s + LEFT JOIN candidate c ON (c.CandID = s.CandID) + WHERE s.Visit_label = :visit + AND s.CandID = :candID + AND c.PSCID = :pscid + ", + [ + 'visit' => $visitLabel, + 'candID' => $candID, + 'pscid' => $pscid + ] + ); + + if (is_null($sessionID)) { + return new \LORIS\Http\Response\JSON\BadRequest( + 'Visit for candidate does not exist' + ); + } + + $config = \NDB_Config::singleton(); + $filename = urldecode($uploadedFile->getClientFilename()); + + if ($filename !== "{$pscid}_{$candID}_{$visitLabel}_bids.tar.gz") { + return new \LORIS\Http\Response\JSON\BadRequest( + 'Filename does not match expected name, please verify.' + ); + } + + $targetdir = new \SplFileInfo($config->getSetting('EEGUploadIncomingPath')); + try { + $uploader = (new \LORIS\FilesUploadHandler($targetdir)); + } catch (\Exception $e) { + return new \LORIS\Http\Response\JSON\InternalServerError( + $e->getMessage() + ); + } + + // Check for existing files + $targetPath = $targetdir->getPathname() . '/' . $filename; + if (file_exists($targetPath)) { + $archiveFilename = str_replace( + '_bids', + '_bids_'.time(), + $filename + ); + + $archivesDir = $targetdir->getPathname() . '/archives/'; + if (!file_exists($archivesDir)) { + mkdir($archivesDir); + } + + $archivePath = $archivesDir . $archiveFilename; + rename($targetPath, $archivePath); + $db->update( + 'electrophysiology_uploader', + ['UploadLocation' => "archives/$archiveFilename"], + ['UploadLocation' => $filename] + ); + } + + $response = $uploader->handle($request); + if (!in_array($response->getStatusCode(), [200, 201], true)) { + // Something went wrong. Return early to skip further processing. + return $response; + } + + $user = $request->getAttribute('user'); + + $saveValues = [ + 'UploadedBy' => $user->getUsername(), + 'UploadDate' => date('Y-m-d H:i:s'), + 'UploadLocation' => "$filename", + 'SessionID' => $sessionID, + 'MetaData' => $values['metaData'] + ]; + + $db->insert('electrophysiology_uploader', $saveValues); + + // Send notification to specific users + $to_list = [ + 'santiago.morales@usc.edu', + 'lyoder@umd.edu', + 'fox@umd.edu', + 'mmcsw1@umd.edu', + 'mantunez@umd.edu' + ]; + $timepoint = \TimePoint::singleton(new \SessionID($sessionID)); + $msg_data = [ + 'Site' => $timepoint->getPSC(), + 'PSCID' => $pscid, + 'CandID' => $candID, + 'VisitLabel' => $visitLabel, + 'UploadedBy' => $saveValues['UploadedBy'], + 'UploadDate' => $saveValues['UploadDate'], + 'UploadLocation' => $saveValues['UploadLocation'], + 'MetaData' => $values['metaData'], + 'UploadUrl' => null, + ]; + + foreach ($to_list as $email) { + \Email::send( + $email, + 'new_electrophysiology_upload.tpl', + $msg_data + ); + } + + return $response; + } +} diff --git a/modules/electrophysiology_uploader/php/verify.class.inc b/modules/electrophysiology_uploader/php/verify.class.inc new file mode 100644 index 00000000000..74bc17f21da --- /dev/null +++ b/modules/electrophysiology_uploader/php/verify.class.inc @@ -0,0 +1,72 @@ +hasPermission('electrophysiology_browser_view_allsites') + || ($user->hasPermission('electrophysiology_browser_view_site') + && $user->hasStudySite() + ) + ); + } + + /** + * Return an array of valid HTTP methods for this endpoint + * + * @return string[] Valid versions + */ + protected function allowedMethods(): array + { + return [ + 'GET', + ]; + } + + /** + * This function will return a json response. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + // Ensure GET or POST request. + switch ($request->getMethod()) { + case 'GET': + return $this->_handleGET($request); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + /** + * Initialize setup, the extra values for the + * create timepoint form. (psc & errors) + * + * @param ServerRequestInterface $request The incoming PSR7 request. + * + * @return ResponseInterface The outgoing PSR7 response + */ + private function _handleGET(ServerRequestInterface $request) : ResponseInterface + { + return new \LORIS\Http\Response\JsonResponse(['username' => $request->getAttribute('user')->getUsername()], 204); + } +}