From e4243a84c35eb81be369458cfabffb5021acbf35 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 30 Jul 2025 14:30:24 -0400 Subject: [PATCH 01/40] [statistics] Cleanup filters and add Registration Date --- .../jsx/widgets/helpers/chartBuilder.js | 8 +++--- .../jsx/widgets/helpers/queryChartForm.js | 26 ++++++++++++++++--- modules/statistics/php/charts.class.inc | 8 +++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/modules/statistics/jsx/widgets/helpers/chartBuilder.js b/modules/statistics/jsx/widgets/helpers/chartBuilder.js index b3b51d1f2c3..7606d764c5e 100644 --- a/modules/statistics/jsx/widgets/helpers/chartBuilder.js +++ b/modules/statistics/jsx/widgets/helpers/chartBuilder.js @@ -130,7 +130,7 @@ const createBarChart = (t, labels, columns, id, targetModal, colours, dataType) axis: { x: { type: 'category', - categories: labels, + categories: labels, }, y: { label: { @@ -226,10 +226,10 @@ const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => name = nameFormat(d[i].name); value = valueFormat(d[i].value, d[i].ratio, d[i].id, d[i].index); - + // Calculate percentage based on grand total of entire dataset let percentage = grandTotal > 0 ? ((d[i].value / grandTotal) * 100).toFixed(1) : 0; - + bgcolor = $$.levelColor ? $$.levelColor(d[i].value) : color(d[i].id); text += ""; @@ -315,7 +315,7 @@ const setupCharts = async (t, targetIsModal, chartDetails, totalLabel) => { if (chart.chartType === 'pie') { chartObject = createPieChart(columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours); } else if (chart.chartType === 'bar') { - chartObject = createBarChart(t, labels, columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours, chart.dataType); + chartObject = createBarChart(labels, columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours, chart.dataType); } else if (chart.chartType === 'line') { chartObject = createLineChart(chartData, columns, `#${chartID}`, chart.label, targetIsModal && '#dashboardModal', chart.titlePrefix); } diff --git a/modules/statistics/jsx/widgets/helpers/queryChartForm.js b/modules/statistics/jsx/widgets/helpers/queryChartForm.js index 9d09cb0b6c1..5d5f0729fea 100644 --- a/modules/statistics/jsx/widgets/helpers/queryChartForm.js +++ b/modules/statistics/jsx/widgets/helpers/queryChartForm.js @@ -51,9 +51,27 @@ const QueryChartForm = (props) => { // Handle clear selection if (normalizedValue.includes('__clear__')) { normalizedValue = undefined; + } else if (normalizedValue.length > 0) { + normalizedValue = '(' + + normalizedValue.map((val) => `'${val}'`).join(',') + ')'; } } + setFormDataObj((prevState) => { + const newFormData = { + ...prevState, + [formElement]: normalizedValue, + }; + if ( + (normalizedValue !== undefined + || prevState[formElement] !== undefined) + && !(formElement.includes('date') && value < '1900-01-01') + ) { + props.callback(newFormData); + } + return newFormData; + }); + const newFormData = { ...formDataObj, [formElement]: normalizedValue, @@ -206,8 +224,8 @@ const QueryChartForm = (props) => { display: 'block'}}> {t('Date Registered', {ns: 'statistics'})} { setFormData(name, value); }} @@ -217,8 +235,8 @@ const QueryChartForm = (props) => { label={t('Range Start', {ns: 'statistics'})} /> { setFormData(name, value); }} diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index a9d39d04cb1..2ff579a7c14 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -755,17 +755,17 @@ class Charts extends \NDB_Page } } - if (($queryParams['dateRegisteredStart'] ?? "undefined") != 'undefined') { + if (($queryParams['dateParticipantRegisteredStart'] ?? "undefined") != 'undefined') { $candJoin = "JOIN candidate c ON c.ID=s.CandidateID"; $paramName = 'dateStart' . (++$paramCounter); $projectQuery .= " AND c.Date_registered >= :$paramName"; - $params[$paramName] = $queryParams['dateRegisteredStart']; + $params[$paramName] = $queryParams['dateParticipantRegisteredStart']; } - if (($queryParams['dateRegisteredEnd'] ?? "undefined") != 'undefined') { + if (($queryParams['dateParticipantRegisteredEnd'] ?? "undefined") != 'undefined') { $candJoin = "JOIN candidate c ON c.ID=s.CandidateID"; $paramName = 'dateEnd' . (++$paramCounter); $projectQuery .= " AND c.Date_registered <= :$paramName"; - $params[$paramName] = $queryParams['dateRegisteredEnd']; + $params[$paramName] = $queryParams['dateParticipantRegisteredEnd']; } return [ From 3f14d60eb5383857d754663287ec1dc6eab56402 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 30 Jul 2025 15:24:20 -0400 Subject: [PATCH 02/40] Fix line length --- modules/statistics/jsx/widgets/helpers/queryChartForm.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/statistics/jsx/widgets/helpers/queryChartForm.js b/modules/statistics/jsx/widgets/helpers/queryChartForm.js index 5d5f0729fea..2b2837e4f41 100644 --- a/modules/statistics/jsx/widgets/helpers/queryChartForm.js +++ b/modules/statistics/jsx/widgets/helpers/queryChartForm.js @@ -224,8 +224,8 @@ const QueryChartForm = (props) => { display: 'block'}}> {t('Date Registered', {ns: 'statistics'})} { setFormData(name, value); }} @@ -235,8 +235,8 @@ const QueryChartForm = (props) => { label={t('Range Start', {ns: 'statistics'})} /> { setFormData(name, value); }} From 8355dd05a811bb40ed16c83d88b155547dd4b65a Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 31 Jul 2025 11:22:05 -0400 Subject: [PATCH 03/40] Add subtitles to dashboard panels --- htdocs/bootstrap/css/custom-css.css | 10 ++++++++++ jsx/Panel.js | 1 - modules/statistics/jsx/widgets/recruitment.js | 1 + modules/statistics/jsx/widgets/studyprogression.js | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/htdocs/bootstrap/css/custom-css.css b/htdocs/bootstrap/css/custom-css.css index e5dc3359071..98ea4837b29 100644 --- a/htdocs/bootstrap/css/custom-css.css +++ b/htdocs/bootstrap/css/custom-css.css @@ -779,6 +779,16 @@ a.btn.btn-primary:hover:not(.download, .split-nav) { padding-right: 10px; } +.panel-subtitle { + border: 1px solid #246EB6; + color: #246EB6; + padding: 1px 5px; + border-radius: 3px; + background-color: white; + font-size: 14px; + margin-left: 10px; +} + .panel-default { border-color: #C3D5DB; } diff --git a/jsx/Panel.js b/jsx/Panel.js index 0fca9987bba..6fc260c57c2 100644 --- a/jsx/Panel.js +++ b/jsx/Panel.js @@ -153,7 +153,6 @@ Panel.propTypes = { bold: PropTypes.bool, panelSize: PropTypes.string, style: PropTypes.object, - children: PropTypes.node, }; Panel.defaultProps = { initCollapsed: false, diff --git a/modules/statistics/jsx/widgets/recruitment.js b/modules/statistics/jsx/widgets/recruitment.js index 77812611baa..d56252017ba 100644 --- a/modules/statistics/jsx/widgets/recruitment.js +++ b/modules/statistics/jsx/widgets/recruitment.js @@ -149,6 +149,7 @@ const Recruitment = (props) => { setChartDetails); }; + // Helper functions to calculate totals for each view const getTotalProjectsCount = () => { return Object.keys(json['recruitment'] || {}) .filter((key) => key !== 'overall').length; diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 70d6962c1bc..7046c7b849f 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -96,6 +96,7 @@ const StudyProgression = (props) => { const filterLabel = (hide) => hide ? t('Hide Filters', {ns: 'loris'}) : t('Show Filters', {ns: 'loris'}); + return loading ? : ( <> Date: Thu, 31 Jul 2025 13:47:39 -0400 Subject: [PATCH 04/40] Fix formatting --- modules/statistics/php/widgets.class.inc | 1 - raisinbread/RB_files/RB_candidate.sql | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index 56d307b0cf6..d88c1dc6d03 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -97,7 +97,6 @@ class Widgets extends \NDB_Page implements ETagCalculator [] ); $recruitment = [ - 'overall' => $this->_createProjectProgressBar( 'overall', dgettext("statistics", "Overall Recruitment"), $recruitmentTarget, diff --git a/raisinbread/RB_files/RB_candidate.sql b/raisinbread/RB_files/RB_candidate.sql index 0941566a003..50b6c650483 100644 --- a/raisinbread/RB_files/RB_candidate.sql +++ b/raisinbread/RB_files/RB_candidate.sql @@ -673,4 +673,4 @@ INSERT INTO `candidate` (`ID`, `CandID`, `PSCID`, `ExternalID`, `DoB`, `DoD`, `E INSERT INTO `candidate` (`ID`, `CandID`, `PSCID`, `ExternalID`, `DoB`, `DoD`, `EDC`, `Sex`, `RegistrationCenterID`, `RegistrationProjectID`, `Ethnicity`, `Active`, `Date_active`, `RegisteredBy`, `UserID`, `Date_registered`, `flagged_caveatemptor`, `flagged_reason`, `flagged_other`, `flagged_other_status`, `Testdate`, `Entity_type`, `ProbandSex`, `ProbandDoB`) VALUES (1017,749066,'scanner',NULL,NULL,NULL,NULL,NULL,3,4,NULL,'Y','2016-06-15',NULL,'NeuroDB::MRI','2016-06-15','false',NULL,NULL,NULL,'2019-07-04 09:42:58','Scanner',NULL,NULL); INSERT INTO `candidate` (`ID`, `CandID`, `PSCID`, `ExternalID`, `DoB`, `DoD`, `EDC`, `Sex`, `RegistrationCenterID`, `RegistrationProjectID`, `Ethnicity`, `Active`, `Date_active`, `RegisteredBy`, `UserID`, `Date_registered`, `flagged_caveatemptor`, `flagged_reason`, `flagged_other`, `flagged_other_status`, `Testdate`, `Entity_type`, `ProbandSex`, `ProbandDoB`) VALUES (1018,965878,'scanner',NULL,NULL,NULL,NULL,NULL,4,4,NULL,'Y','2016-06-26',NULL,'NeuroDB::MRI','2016-06-26','false',NULL,NULL,NULL,'2019-07-04 09:42:58','Scanner',NULL,NULL); UNLOCK TABLES; -SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file +SET FOREIGN_KEY_CHECKS=1; From 69994e230d19cc7bad465769d14f4a5aa730d64c Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 31 Jul 2025 13:59:04 -0400 Subject: [PATCH 05/40] Fix tests --- modules/statistics/php/widgets.class.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index d88c1dc6d03..56d307b0cf6 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -97,6 +97,7 @@ class Widgets extends \NDB_Page implements ETagCalculator [] ); $recruitment = [ + 'overall' => $this->_createProjectProgressBar( 'overall', dgettext("statistics", "Overall Recruitment"), $recruitmentTarget, From c45fa23cec38acf22edb0023688fcb729509a971 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 13 Aug 2025 13:41:01 -0400 Subject: [PATCH 06/40] Fix issue with visit filter + console warning + date_registered --- modules/statistics/jsx/widgets/helpers/queryChartForm.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/statistics/jsx/widgets/helpers/queryChartForm.js b/modules/statistics/jsx/widgets/helpers/queryChartForm.js index 2b2837e4f41..2f58aa24a33 100644 --- a/modules/statistics/jsx/widgets/helpers/queryChartForm.js +++ b/modules/statistics/jsx/widgets/helpers/queryChartForm.js @@ -51,9 +51,6 @@ const QueryChartForm = (props) => { // Handle clear selection if (normalizedValue.includes('__clear__')) { normalizedValue = undefined; - } else if (normalizedValue.length > 0) { - normalizedValue = '(' - + normalizedValue.map((val) => `'${val}'`).join(',') + ')'; } } From 22e70830b90f0630b2e0aa94cf09594f8d9e3acf Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 14 Aug 2025 12:16:28 -0400 Subject: [PATCH 07/40] Reorganize and add age distribution --- htdocs/bootstrap/css/custom-css.css | 9 ++------- modules/statistics/css/WidgetIndex.css | 2 +- modules/statistics/css/recruitment.css | 2 +- modules/statistics/jsx/widgets/helpers/chartBuilder.js | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/htdocs/bootstrap/css/custom-css.css b/htdocs/bootstrap/css/custom-css.css index 98ea4837b29..a55221c1238 100644 --- a/htdocs/bootstrap/css/custom-css.css +++ b/htdocs/bootstrap/css/custom-css.css @@ -780,13 +780,8 @@ a.btn.btn-primary:hover:not(.download, .split-nav) { } .panel-subtitle { - border: 1px solid #246EB6; - color: #246EB6; - padding: 1px 5px; - border-radius: 3px; - background-color: white; - font-size: 14px; - margin-left: 10px; + font-size: 0.8em; + margin-left: 5px } .panel-default { diff --git a/modules/statistics/css/WidgetIndex.css b/modules/statistics/css/WidgetIndex.css index eb3179bc695..ff68a6b6c96 100644 --- a/modules/statistics/css/WidgetIndex.css +++ b/modules/statistics/css/WidgetIndex.css @@ -206,4 +206,4 @@ .filter-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} diff --git a/modules/statistics/css/recruitment.css b/modules/statistics/css/recruitment.css index 1aaccf5781d..8825797b107 100644 --- a/modules/statistics/css/recruitment.css +++ b/modules/statistics/css/recruitment.css @@ -42,4 +42,4 @@ transform: translateY(-10px); background-color: #f9fdff; box-shadow: 0 12px 12px rgba(0, 0, 0, 0.25); -} \ No newline at end of file +} diff --git a/modules/statistics/jsx/widgets/helpers/chartBuilder.js b/modules/statistics/jsx/widgets/helpers/chartBuilder.js index 7606d764c5e..4e85b21619f 100644 --- a/modules/statistics/jsx/widgets/helpers/chartBuilder.js +++ b/modules/statistics/jsx/widgets/helpers/chartBuilder.js @@ -179,7 +179,7 @@ const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => columns: columns, type: 'area-spline', }, - spline: {interpolation: {type: 'monotone'}}, + spline: {interpolation: {type: 'monotone'}} axis: id.includes('bymonth') && { x: { type: 'timeseries', From c3b9c2469bea898a45f10b94a6a0da15f3e1fd58 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 14 Aug 2025 12:24:20 -0400 Subject: [PATCH 08/40] Fix php formatting --- modules/statistics/php/charts.class.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 2ff579a7c14..c685b494330 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -346,6 +346,7 @@ class Charts extends \NDB_Page if (!isset($agesByProject[$projectID]['ages'])) { $agesByProject[$projectID]['ages'] = []; } + if (!isset($agesByProject[$projectID]['ages'][$age])) { $agesByProject[$projectID]['ages'][$age] = 1; } else { From 8f148d64433b9e0a7116738ef8cc34f1e306c56f Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Thu, 14 Aug 2025 13:09:45 -0400 Subject: [PATCH 09/40] Just trying to make phan happy --- modules/statistics/php/charts.class.inc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index c685b494330..c299d6573fc 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -352,6 +352,12 @@ class Charts extends \NDB_Page } else { $agesByProject[$projectID]['ages'][$age]++; } + + if (!isset($agesByProject[$projectID]['ages'][$age])) { + $agesByProject[$projectID]['ages'][$age] = 0; + } + + $agesByProject[$projectID]['ages'][$age]++; } // Create sorted labels (individual ages) From 49b9729530593aba39fe25d024b3cb6548244db4 Mon Sep 17 00:00:00 2001 From: Saagar Arya Date: Wed, 3 Sep 2025 15:24:54 -0400 Subject: [PATCH 10/40] Add % to age chart and bigger subtitle --- htdocs/bootstrap/css/custom-css.css | 5 ----- modules/statistics/jsx/widgets/helpers/chartBuilder.js | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/htdocs/bootstrap/css/custom-css.css b/htdocs/bootstrap/css/custom-css.css index a55221c1238..e5dc3359071 100644 --- a/htdocs/bootstrap/css/custom-css.css +++ b/htdocs/bootstrap/css/custom-css.css @@ -779,11 +779,6 @@ a.btn.btn-primary:hover:not(.download, .split-nav) { padding-right: 10px; } -.panel-subtitle { - font-size: 0.8em; - margin-left: 5px -} - .panel-default { border-color: #C3D5DB; } diff --git a/modules/statistics/jsx/widgets/helpers/chartBuilder.js b/modules/statistics/jsx/widgets/helpers/chartBuilder.js index 4e85b21619f..7b45e44c151 100644 --- a/modules/statistics/jsx/widgets/helpers/chartBuilder.js +++ b/modules/statistics/jsx/widgets/helpers/chartBuilder.js @@ -167,6 +167,7 @@ const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => } } } + let newChart = c3.generate({ size: { height: targetModal && 500, @@ -179,7 +180,7 @@ const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => columns: columns, type: 'area-spline', }, - spline: {interpolation: {type: 'monotone'}} + spline: {interpolation: {type: 'monotone'}}, axis: id.includes('bymonth') && { x: { type: 'timeseries', From 7389b4bb35b99d43b840ae3b62fb18f0243b49dd Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 15 Sep 2025 16:37:28 -0400 Subject: [PATCH 11/40] Add project size charts + Generalize helpers --- modules/behavioural_qc/php/module.class.inc | 10 +-- modules/candidate_list/php/module.class.inc | 10 +-- modules/dqt/php/module.class.inc | 55 ++++++++++++ modules/imaging_browser/php/module.class.inc | 6 +- .../jsx/widgets/helpers/chartBuilder.js | 21 +++-- modules/statistics/jsx/widgets/recruitment.js | 5 ++ .../jsx/widgets/studyprogression.js | 60 +++++++++++++ modules/statistics/php/charts.class.inc | 39 ++++++++ modules/statistics/php/widgets.class.inc | 25 +++++- tools/update_projects_disk_space.php | 90 +++++++++++++++++++ 10 files changed, 301 insertions(+), 20 deletions(-) create mode 100644 tools/update_projects_disk_space.php diff --git a/modules/behavioural_qc/php/module.class.inc b/modules/behavioural_qc/php/module.class.inc index ed877b42bb6..0792d17fae5 100644 --- a/modules/behavioural_qc/php/module.class.inc +++ b/modules/behavioural_qc/php/module.class.inc @@ -78,18 +78,18 @@ class Module extends \Module case 'study-progression': $DB = $factory->database(); $data = $DB->pselectWithIndexKey( - "SELECT + "SELECT p.ProjectID, COUNT(*) AS count, CONCAT('$baseURL/behavioural_qc/?Project=', p.ProjectID) AS url, p.Name AS ProjectName - FROM flag f - JOIN session s ON f.SessionID = s.ID + FROM flag f + JOIN session s ON f.SessionID = s.ID JOIN Project p ON p.ProjectID = s.ProjectID WHERE DataID IS NOT NULL - AND s.Active <> 'N' - AND s.CenterID <> 1 + AND s.Active <> 'N' + AND s.CenterID <> 1 AND f.CommentID NOT LIKE 'DDE_%' GROUP BY p.Name", [], diff --git a/modules/candidate_list/php/module.class.inc b/modules/candidate_list/php/module.class.inc index f83c12db85f..15821f713dd 100644 --- a/modules/candidate_list/php/module.class.inc +++ b/modules/candidate_list/php/module.class.inc @@ -121,16 +121,16 @@ class Module extends \Module AS url, ProjectName FROM ( - SELECT + SELECT c.PSCID, COALESCE(p.ProjectID, p2.ProjectID) AS ProjectID, COALESCE(p.Name, p2.Name) AS ProjectName FROM candidate c - LEFT JOIN session s ON s.CandidateID = c.ID + LEFT JOIN session s ON s.CandidateID = c.ID LEFT JOIN Project p ON p.ProjectID = s.ProjectID - JOIN Project p2 ON c.RegistrationProjectID = p2.ProjectID - WHERE c.Active <> 'N' - AND s.Active <> 'N' + JOIN Project p2 ON c.RegistrationProjectID = p2.ProjectID + WHERE c.Active <> 'N' + AND s.Active <> 'N' AND s.CenterID <> 1 ) AS sub GROUP BY ProjectID, ProjectName;", diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 2e00f6a4021..7c26efba059 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -58,4 +58,59 @@ class Module extends \Module { return dgettext("dqt", "Data Query Tool"); } + + /** + * {@inheritDoc} + * + * @param string $type The type of widgets to get. + * @param \User $user The user widgets are being retrieved for. + * @param array $options A type dependent list of options to provide + * to the widget. + * + * @return \LORIS\GUI\Widget[] + */ + public function getWidgets(string $type, \User $user, array $options) : array + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + $projects = $user->getProjects(); + + switch ($type) { + case 'study-progression': + $DB = $factory->database(); + $cachedSizeData = json_decode( + html_entity_decode($DB->pselectOne( + "SELECT Value + FROM cached_data + JOIN cached_data_type USING (CachedDataTypeID) + WHERE Name='projects_disk_space'", + [] + )), + true + ); + + $data = []; + foreach ($projects as $project) { + $projectName = $project->getName(); + if (in_array($projectName, array_keys($cachedSizeData))) { + $data[] = [ + 'ProjectID' => $project->getId(), + 'count' => $cachedSizeData[$projectName]['total'] . ' GB', + 'url' => "$baseURL/dqt", + 'ProjectName' => $projectName + ]; + } + } + + return [ + new \LORIS\dashboard\DataWidget( + "Project Size", + $data, + "", + 'rgb(186,225,255)', + ) + ]; + } + return []; + } } diff --git a/modules/imaging_browser/php/module.class.inc b/modules/imaging_browser/php/module.class.inc index bd848130929..4e577d48445 100644 --- a/modules/imaging_browser/php/module.class.inc +++ b/modules/imaging_browser/php/module.class.inc @@ -129,15 +129,15 @@ class Module extends \Module case 'study-progression': $DB = $factory->database(); $data = $DB->pselectWithIndexKey( - "SELECT + "SELECT p.ProjectID, p.Name AS ProjectName, COUNT(s.ID) AS count, - '".$baseURL."/imaging_browser' as url + CONCAT('".$baseURL."/imaging_browser/?project=', p.Name) as url FROM session s JOIN Project p ON p.ProjectID = s.ProjectID JOIN mri_upload mu ON mu.SessionID = s.ID - WHERE s.Active <> 'N' + WHERE s.Active <> 'N' AND s.CenterID <> 1 GROUP BY p.Name", [], diff --git a/modules/statistics/jsx/widgets/helpers/chartBuilder.js b/modules/statistics/jsx/widgets/helpers/chartBuilder.js index 7b45e44c151..c5a7b7dda92 100644 --- a/modules/statistics/jsx/widgets/helpers/chartBuilder.js +++ b/modules/statistics/jsx/widgets/helpers/chartBuilder.js @@ -76,7 +76,7 @@ const formatBarData = (data) => { return processedData; }; -const createPieChart = (columns, id, targetModal, colours) => { +const createPieChart = (columns, id, targetModal, colours, units = null, showPieLabelRatio = true) => { let newChart = c3.generate({ bindto: targetModal ? targetModal : id, data: { @@ -92,13 +92,22 @@ const createPieChart = (columns, id, targetModal, colours) => { pie: { label: { format: function(value, ratio, id) { - return value + "("+Math.round(100*ratio)+"%)"; + if (units) { + value = `${value} ${units}`; + } + if (showPieLabelRatio) { + value = `${value} (${(ratio * 100).toFixed(0)}%)`; + } + return value; } } }, tooltip: { format: { value: function (value, ratio) { + if (units) { + value = `${value} ${units}`; + } return `${value} (${(ratio * 100).toFixed(0)}%)`; }, }, @@ -107,7 +116,7 @@ const createPieChart = (columns, id, targetModal, colours) => { return newChart; } -const createBarChart = (t, labels, columns, id, targetModal, colours, dataType) => { +const createBarChart = (labels, columns, id, targetModal, colours, dataType, yLabel) => { let newChart = c3.generate({ bindto: targetModal ? targetModal : id, data: { @@ -134,7 +143,7 @@ const createBarChart = (t, labels, columns, id, targetModal, colours, dataType) }, y: { label: { - text: t('Candidates registered', { ns: 'statistics'}), + text: yLabel, position: 'inner-top' }, }, @@ -314,9 +323,9 @@ const setupCharts = async (t, targetIsModal, chartDetails, totalLabel) => { } let chartObject = null; if (chart.chartType === 'pie') { - chartObject = createPieChart(columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours); + chartObject = createPieChart(columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours, chart.units, chart.showPieLabelRatio); } else if (chart.chartType === 'bar') { - chartObject = createBarChart(labels, columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours, chart.dataType); + chartObject = createBarChart(labels, columns, `#${chartID}`, targetIsModal && '#dashboardModal', colours, chart.dataType, chart.yLabel); } else if (chart.chartType === 'line') { chartObject = createLineChart(chartData, columns, `#${chartID}`, chart.label, targetIsModal && '#dashboardModal', chart.titlePrefix); } diff --git a/modules/statistics/jsx/widgets/recruitment.js b/modules/statistics/jsx/widgets/recruitment.js index d56252017ba..423b02bb25e 100644 --- a/modules/statistics/jsx/widgets/recruitment.js +++ b/modules/statistics/jsx/widgets/recruitment.js @@ -32,6 +32,7 @@ const Recruitment = (props) => { dataType: 'pie', label: 'Age (Years)', options: {pie: 'pie', bar: 'bar'}, + yLabel: 'Candidates registered', legend: 'under', chartObject: null, }, @@ -42,6 +43,7 @@ const Recruitment = (props) => { dataType: 'pie', label: 'Ethnicity', options: {pie: 'pie', bar: 'bar'}, + yLabel: 'Candidates registered', legend: 'under', chartObject: null, }, @@ -55,6 +57,7 @@ const Recruitment = (props) => { label: 'Participants', legend: '', options: {pie: 'pie', bar: 'bar'}, + yLabel: 'Candidates registered', chartObject: null, }, 'siterecruitment_bysex': { @@ -64,6 +67,7 @@ const Recruitment = (props) => { dataType: 'bar', legend: 'under', options: {bar: 'bar', pie: 'pie'}, + yLabel: 'Candidates registered', chartObject: null, }, }, @@ -75,6 +79,7 @@ const Recruitment = (props) => { dataType: 'line', legend: '', options: {line: 'line'}, + yLabel: 'Candidates registered', chartObject: null, }, }, diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 7046c7b849f..c163b914a96 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -45,6 +45,7 @@ const StudyProgression = (props) => { legend: 'under', options: {line: 'line'}, chartObject: null, + yLabel: 'Candidates registered', titlePrefix: 'Month', }, }, @@ -58,9 +59,27 @@ const StudyProgression = (props) => { legend: '', options: {line: 'line'}, chartObject: null, + yLabel: 'Candidates registered', titlePrefix: 'Month', }, }, + 'project_sizes': { // This should be a class + 'size_byproject': { + sizing: 11, + title: 'Size breakdown by project', + filters: '', + chartType: 'pie', + dataType: 'pie', + label: 'Size (GB)', + units: 'GB', + showPieLabelRatio: false, + legend: '', + options: {pie: 'pie', bar: 'bar'}, + chartObject: null, + yLabel: 'Size (GB)', + titlePrefix: 'Project', + }, + }, }); const showChart = ((section, chartID) => { @@ -82,6 +101,7 @@ const StudyProgression = (props) => { setChartDetails(data); }); json = props.data; + console.log('thejson', json); setLoading(false); } }, [props.data, t]); @@ -218,6 +238,46 @@ const StudyProgression = (props) => { title: title('Site Recruitment'), onToggleFilters: () => showFiltersBreakdown((prev) => !prev), }, + { + content: + Object.keys(json['options']['projects']).length > 0 ? ( +
+
+ {/* setShowFiltersBreakdown((prev) => !prev)}*/} + {/*>*/} + {/* {showFiltersBreakdown ? 'Hide Filters' : 'Show Filters'}*/} + {/**/} +
+ {showFiltersBreakdown && ( + { + updateFilters(formDataObj, 'project_sizes'); + }} + /> + )} + {showChart('project_sizes', 'size_byproject')} +
+ ) : ( +

There is no data yet.

+ ), + title: 'Study Progression - project sizes', + subtitle: 'Total size: ' + + (json['studyprogression']['total_size'] ?? -1) + + ' GB', + }, ]} /> diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index c299d6573fc..18cdfc458d4 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -100,6 +100,8 @@ class Charts extends \NDB_Page return $this->_handleScansByMonth($request); case 'siterecruitment_bymonth': return $this->_handleSiteLineData($request); + case 'size_byproject': + return $this->_handleProjectSizeBreakdown(); case 'agedistribution_line': return $this->_handleAgeDistributionByProject($request); default: @@ -838,4 +840,41 @@ class Charts extends \NDB_Page } return $data; } + + + /** + * Handle an incoming request for project size breakdown. + * + * @return ResponseInterface + */ + private function _handleProjectSizeBreakdown() + { + $DB = \NDB_Factory::singleton()->database(); + $user = \NDB_Factory::singleton()->user(); + $projects = $user->getProjects(); + + $cachedSizeData = json_decode( + html_entity_decode($DB->pselectOne( + "SELECT Value + FROM cached_data + JOIN cached_data_type USING (CachedDataTypeID) + WHERE Name='projects_disk_space'", + [] + )), + true + ); + + $projectData = []; + foreach ($projects as $project) { + $projectName = $project->getName(); + if (in_array($projectName, array_keys($cachedSizeData))) { + $projectData[] = [ + 'label' => $projectName, + ...$cachedSizeData[$projectName], + ]; + } + } + + return (new \LORIS\Http\Response\JsonResponse($projectData)); + } } diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index 56d307b0cf6..2b2c8771645 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -119,6 +119,19 @@ class Widgets extends \NDB_Page implements ETagCalculator $studyWidgets ); + $totalSizeOfProjectsGB = 0; + $projectsSizes = []; + $cachedSizeData = json_decode( + html_entity_decode($db->pselectOne( + "SELECT Value + FROM cached_data + JOIN cached_data_type USING (CachedDataTypeID) + WHERE Name='projects_disk_space'", + [] + )), + true + ); + foreach ($projects as $pid) { // Set project recruitment data $projectInfo = $config->getProjectSettings(intval(strval($pid))); @@ -129,9 +142,10 @@ class Widgets extends \NDB_Page implements ETagCalculator 'project ID ' . intval(strval($pid)) ); } + $projectName = $projectInfo['Name']; $recruitment[intval(strval($pid))] = $this->_createProjectProgressBar( strval($pid), - $projectInfo['Name'], + $projectName, $projectInfo['recruitmentTarget'], $this->getTotalRecruitmentByProject( $recruitmentRaw, @@ -174,6 +188,12 @@ class Widgets extends \NDB_Page implements ETagCalculator $visitOptions[$visitLabel] = $visitName; } } + + if (in_array($projectName, array_keys($cachedSizeData))) { + $projectSize = $cachedSizeData[$projectName]['total']; + $projectsSizes[$projectName] = $projectSize; + $totalSizeOfProjectsGB += floatval($projectSize); + } } $siteOptions = []; @@ -201,10 +221,13 @@ class Widgets extends \NDB_Page implements ETagCalculator 'total_scans' => $totalScans, 'recruitment' => $recruitment, 'progressionData' => $studyProgressionProjects, + 'total_size' => $totalSizeOfProjectsGB, ]; $values['options'] = $options; $values['recruitmentcohorts'] = $recruitmentCohorts; + $values['size_byproject'] = $projectsSizes; + $this->_cache = new \LORIS\Http\Response\JsonResponse($values); return $this->_cache; diff --git a/tools/update_projects_disk_space.php b/tools/update_projects_disk_space.php new file mode 100644 index 00000000000..517d92dc1d7 --- /dev/null +++ b/tools/update_projects_disk_space.php @@ -0,0 +1,90 @@ +database(); +$config = \NDB_Config::singleton(); + +$dataDir = $config->getSetting('dataDirBasepath'); +$list_of_projects = Utility::getProjectList(); + +$project_data = []; +// NOTE: Below is EEGNet specific. Other projects may want to use 'files' +// or a different query +foreach ($list_of_projects as $pid => $project) { + $project_directory = $db->pselectOne( + "SELECT DISTINCT( + SUBSTRING(SUBSTRING_INDEX(FilePath, '/', 2), 1) + ) + FROM physiological_file pf + LEFT JOIN session s ON s.ID = pf.SessionID + WHERE s.ProjectID=:PID LIMIT 1;", + ['PID' => $pid] + ); + if (str_starts_with($project_directory, 'bids_imports')) { + $full_path = $dataDir . $project_directory; + $dir_size_gb = round( + get_dir_size($full_path) / pow(10, 9), + 1 + ); + $project_data[$project]['total'] = $dir_size_gb; + } +} + +$cached_data_type_id = $db->pselectOneInt( + "SELECT `CachedDataTypeID` + FROM `cached_data_type` + WHERE `Name` = 'projects_disk_space'", + [] +); + +$row_exists = $db->pselectOne( + "SELECT Value FROM cached_data + WHERE CachedDataTypeID = :CDTID;", + ['CDTID' => $cached_data_type_id] +); + +if ($row_exists) { + $db->update( + 'cached_data', + ['Value' => json_encode($project_data)], + ['CachedDataTypeID' => $cached_data_type_id] + ); +} else { + $db->insert( + 'cached_data', + [ + 'CachedDataTypeID' => $cached_data_type_id, + 'Value' => json_encode($project_data) + ] + ); +} + +echo "Disk space updated\r\n"; + + +/** + * Calculate directory size, recursively, skipping .tgz files + * + * @return int + */ +function get_dir_size($directory){ + $size = 0; + $files = glob($directory . '/*'); + foreach($files as $path){ + if (!str_ends_with($path, '.tgz')) { + is_file($path) && $size += filesize($path); + is_dir($path) && $size += get_dir_size($path); + } + } + return $size; +} From 90534f77eede8cc749dd6b1dc83e78d4e57c99ac Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 15 Sep 2025 16:49:40 -0400 Subject: [PATCH 12/40] Add SQL patch + RB --- SQL/0000-00-00-schema.sql | 31 ++++++++++++++++++- .../2025_09_11_add_data_cache_table.sql | 21 +++++++++++++ raisinbread/RB_files/RB_cached_data.sql | 6 ++++ raisinbread/RB_files/RB_cached_data_type.sql | 6 ++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 SQL/New_patches/2025_09_11_add_data_cache_table.sql create mode 100644 raisinbread/RB_files/RB_cached_data.sql create mode 100644 raisinbread/RB_files/RB_cached_data_type.sql diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index ee5061d0c42..19579e6d70c 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -1496,6 +1496,35 @@ INSERT INTO StatisticsTabs (ModuleName, SubModuleName, Description, OrderNo) VAL ('statistics', 'stats_behavioural', 'Behavioural Statistics', 3), ('statistics', 'stats_MRI', 'Imaging Statistics', 4); + +-- ******************************** +-- statistics +-- ******************************** + + +CREATE TABLE `cached_data_type` ( + `CachedDataTypeID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `Name` VARCHAR(255) UNIQUE NOT NULL, + PRIMARY KEY (`CachedDataTypeID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +INSERT INTO `cached_data_type` (`Name`) SELECT 'projects_disk_space'; + + +CREATE TABLE `cached_data` ( + `CachedDataID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `CachedDataTypeID` INT(10) UNSIGNED NOT NULL, + `Value` TEXT NOT NULL, + `LastUpdate` TIMESTAMP NOT NULL + DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`CachedDataID`), + CONSTRAINT `FK_cached_data_type` FOREIGN KEY (`CachedDataTypeID`) + REFERENCES `cached_data_type` (`CachedDataTypeID`) + ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + -- ******************************** -- server_processes tables -- ******************************** @@ -2688,4 +2717,4 @@ CREATE TABLE `redcap_notification` ( `handled_dt` datetime NULL, PRIMARY KEY (`id`), KEY `i_redcap_notif_received_dt` (`received_dt`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/SQL/New_patches/2025_09_11_add_data_cache_table.sql b/SQL/New_patches/2025_09_11_add_data_cache_table.sql new file mode 100644 index 00000000000..aaab6cc3c6d --- /dev/null +++ b/SQL/New_patches/2025_09_11_add_data_cache_table.sql @@ -0,0 +1,21 @@ +-- Create cached_data_type table to track different types of cached data +CREATE TABLE `cached_data_type` ( + `CachedDataTypeID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `Name` VARCHAR(255) UNIQUE NOT NULL, + PRIMARY KEY (`CachedDataTypeID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Create cached_data table to track cached data +CREATE TABLE `cached_data` ( + `CachedDataID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `CachedDataTypeID` INT(10) UNSIGNED NOT NULL, + `Value` TEXT NOT NULL, + `LastUpdate` TIMESTAMP NOT NULL + DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`CachedDataID`), + CONSTRAINT `FK_cached_data_type` FOREIGN KEY (`CachedDataTypeID`) + REFERENCES `cached_data_type` (`CachedDataTypeID`) + ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `cached_data_type` (`Name`) SELECT 'projects_disk_space'; diff --git a/raisinbread/RB_files/RB_cached_data.sql b/raisinbread/RB_files/RB_cached_data.sql new file mode 100644 index 00000000000..cabb8271eed --- /dev/null +++ b/raisinbread/RB_files/RB_cached_data.sql @@ -0,0 +1,6 @@ +SET FOREIGN_KEY_CHECKS=0; +TRUNCATE TABLE `cached_data`; +LOCK TABLES `cached_data` WRITE; +INSERT INTO `cached_data` (`CachedDataID`, `CachedDataTypeID`, `Value`, `LastUpdate`) VALUES (1,1,'{"Pumpernickel":{"total":0.8}}','2025-09-10 11:12:13'); +UNLOCK TABLES; +SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_cached_data_type.sql b/raisinbread/RB_files/RB_cached_data_type.sql new file mode 100644 index 00000000000..e1f1ec608fb --- /dev/null +++ b/raisinbread/RB_files/RB_cached_data_type.sql @@ -0,0 +1,6 @@ +SET FOREIGN_KEY_CHECKS=0; +TRUNCATE TABLE `cached_data_type`; +LOCK TABLES `cached_data_type` WRITE; +INSERT INTO `cached_data_type` (`CachedDataTypeID`, `Name`) VALUES (1,'projects_disk_space'); +UNLOCK TABLES; +SET FOREIGN_KEY_CHECKS=1; From 16126036b143b2a1de168125c794a55df4926ed2 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 15 Sep 2025 17:09:31 -0400 Subject: [PATCH 13/40] Rename 'Project Size' to 'Dataset Size' --- modules/dqt/php/module.class.inc | 2 +- modules/statistics/jsx/widgets/studyprogression.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 7c26efba059..2fe9c5cd000 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -104,7 +104,7 @@ class Module extends \Module return [ new \LORIS\dashboard\DataWidget( - "Project Size", + "Dataset Size", $data, "", 'rgb(186,225,255)', diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index c163b914a96..4c9cbe705d2 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -66,7 +66,7 @@ const StudyProgression = (props) => { 'project_sizes': { // This should be a class 'size_byproject': { sizing: 11, - title: 'Size breakdown by project', + title: 'Dataset size breakdown by project', filters: '', chartType: 'pie', dataType: 'pie', @@ -273,7 +273,7 @@ const StudyProgression = (props) => { ) : (

There is no data yet.

), - title: 'Study Progression - project sizes', + title: 'Study Progression - project dataset sizes', subtitle: 'Total size: ' + (json['studyprogression']['total_size'] ?? -1) + ' GB', From a40e1c77c84d4e4ad88b444085d7a02d78f9d217 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Tue, 16 Sep 2025 16:32:47 -0400 Subject: [PATCH 14/40] Fint php lint --- modules/dqt/php/module.class.inc | 54 +++++++------- .../php/module.class.inc | 49 +++++++++++++ modules/statistics/php/charts.class.inc | 59 ++++++++++++++-- modules/statistics/php/widgets.class.inc | 28 ++++++-- tools/update_projects_disk_space.php | 70 ++++++++++--------- 5 files changed, 193 insertions(+), 67 deletions(-) diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 2fe9c5cd000..24b1741a5d5 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -76,40 +76,44 @@ class Module extends \Module $projects = $user->getProjects(); switch ($type) { - case 'study-progression': - $DB = $factory->database(); - $cachedSizeData = json_decode( - html_entity_decode($DB->pselectOne( + case 'study-progression': + $DB = $factory->database(); + $cachedSizeData = json_decode( + html_entity_decode( + $DB->pselectOne( "SELECT Value FROM cached_data JOIN cached_data_type USING (CachedDataTypeID) WHERE Name='projects_disk_space'", [] - )), - true - ); + ) + ), + true + ); - $data = []; - foreach ($projects as $project) { - $projectName = $project->getName(); - if (in_array($projectName, array_keys($cachedSizeData))) { - $data[] = [ - 'ProjectID' => $project->getId(), - 'count' => $cachedSizeData[$projectName]['total'] . ' GB', - 'url' => "$baseURL/dqt", - 'ProjectName' => $projectName - ]; - } + $data = []; + foreach ($projects as $project) { + $projectName = $project->getName(); + if (!in_array($projectName, array_keys($cachedSizeData))) { + continue; } - return [ - new \LORIS\dashboard\DataWidget( - "Dataset Size", - $data, - "", - 'rgb(186,225,255)', - ) + $data[] = [ + 'ProjectID' => $project->getId(), + 'ProjectName' => $projectName, + 'count' => "{$cachedSizeData[$projectName]['total']} GB", + 'url' => "$baseURL/dqt", ]; + } + + return [ + new \LORIS\dashboard\DataWidget( + "Dataset Size", + $data, + "", + 'rgb(186,225,255)', + ) + ]; } return []; } diff --git a/modules/electrophysiology_browser/php/module.class.inc b/modules/electrophysiology_browser/php/module.class.inc index 98c1719eaa7..bc5ab7d70f1 100644 --- a/modules/electrophysiology_browser/php/module.class.inc +++ b/modules/electrophysiology_browser/php/module.class.inc @@ -63,4 +63,53 @@ class Module extends \Module { return dgettext("electrophysiology_browser", "Electrophysiology Browser"); } + + /** + * {@inheritDoc} + * + * @param string $type The type of widgets to get. + * @param \User $user The user widgets are being retrieved for. + * @param array $options A type dependent list of options to provide + * to the widget. + * + * @return \LORIS\GUI\Widget[] + */ + public function getWidgets(string $type, \User $user, array $options) : array + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + $projects = $user->getProjects(); + + switch ($type) { + case 'study-progression': + $DB = $factory->database(); + $data = $DB->pselectWithIndexKey( + "SELECT + p.ProjectID, + p.Name AS ProjectName, + COUNT(s.ID) AS count, + CONCAT('" + . $baseURL . + "/electrophysiology_browser/?project=', p.Name + ) as url + FROM session s + JOIN Project p ON p.ProjectID = s.ProjectID + JOIN physiological_file pf ON pf.SessionID = s.ID + WHERE s.Active <> 'N' + AND s.CenterID <> 1 + GROUP BY p.Name", + [], + 'ProjectID' + ); + return [ + new \LORIS\dashboard\DataWidget( + "EEG Sessions", + $data, + "", + 'rgb(186,255,201)', + ) + ]; + } + return []; + } } diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 18cdfc458d4..a68abd230f4 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -104,6 +104,8 @@ class Charts extends \NDB_Page return $this->_handleProjectSizeBreakdown(); case 'agedistribution_line': return $this->_handleAgeDistributionByProject($request); + case 'eeg_recordings_by_site': + return $this->_handleSiteEEGRecordingsBreakdown(); default: return new \LORIS\Http\Response\JSON\NotFound(); } @@ -854,13 +856,15 @@ class Charts extends \NDB_Page $projects = $user->getProjects(); $cachedSizeData = json_decode( - html_entity_decode($DB->pselectOne( - "SELECT Value + html_entity_decode( + $DB->pselectOne( + "SELECT Value FROM cached_data JOIN cached_data_type USING (CachedDataTypeID) WHERE Name='projects_disk_space'", - [] - )), + [] + ) + ), true ); @@ -877,4 +881,51 @@ class Charts extends \NDB_Page return (new \LORIS\Http\Response\JsonResponse($projectData)); } + + /** + * Handle an incoming request for EEG recordings by site breakdown. + * + * @return ResponseInterface + */ + private function _handleSiteEEGRecordingsBreakdown() + { + $DB = \NDB_Factory::singleton()->database(); + $user = \NDB_Factory::singleton()->user(); + $sites = $user->getStudySites(); + + $conditions = $this->_buildQueryConditions(); + + $user = \NDB_Factory::singleton()->user(); + + $data = $DB->pselect( + "SELECT s.CenterID, + COUNT(distinct s.ID) as count + FROM physiological_file pf + LEFT JOIN session s ON (s.ID=pf.SessionID) + + GROUP BY s.CenterID + ORDER BY s.CenterID", + [] + ); + + $processed_data = []; + foreach ($data as $row) { + $processed_data[$row['CenterID']] = [ + 'CenterID' => $row['CenterID'], + 'Name' => $row['Name'], + 'count' => intval($row['count']), + ]; + } + $siteData = []; + foreach ($sites as $siteID => $siteName) { + if (in_array($siteID, array_keys($processed_data))) { + $siteData[] = [ + 'label' => $siteName, + 'total' => $processed_data[$siteID]['count'], + ]; + } + } + + return (new \LORIS\Http\Response\JsonResponse($siteData)); + } } diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index 2b2c8771645..9aa547562e1 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -122,13 +122,15 @@ class Widgets extends \NDB_Page implements ETagCalculator $totalSizeOfProjectsGB = 0; $projectsSizes = []; $cachedSizeData = json_decode( - html_entity_decode($db->pselectOne( - "SELECT Value + html_entity_decode( + $db->pselectOne( + "SELECT Value FROM cached_data JOIN cached_data_type USING (CachedDataTypeID) WHERE Name='projects_disk_space'", - [] - )), + [] + ) + ), true ); @@ -190,7 +192,7 @@ class Widgets extends \NDB_Page implements ETagCalculator } if (in_array($projectName, array_keys($cachedSizeData))) { - $projectSize = $cachedSizeData[$projectName]['total']; + $projectSize = $cachedSizeData[$projectName]['total']; $projectsSizes[$projectName] = $projectSize; $totalSizeOfProjectsGB += floatval($projectSize); } @@ -203,6 +205,16 @@ class Widgets extends \NDB_Page implements ETagCalculator } } + $eeg_data = $db->pselectOneInt( + "SELECT COUNT(distinct s.ID) as total_recordings + FROM physiological_file pf + JOIN session s ON (s.ID=pf.SessionID) + WHERE s.ProjectID IN (" . + join($user->getProjectIDs()) + . ")", + [] + ); + $participantStatusOptions = \Candidate::getParticipantStatusOptions(); @@ -226,7 +238,11 @@ class Widgets extends \NDB_Page implements ETagCalculator $values['options'] = $options; $values['recruitmentcohorts'] = $recruitmentCohorts; - $values['size_byproject'] = $projectsSizes; + $values['size_byproject'] = $projectsSizes; + + $values['eeg_data'] = [ + $eeg_data + ]; $this->_cache = new \LORIS\Http\Response\JsonResponse($values); diff --git a/tools/update_projects_disk_space.php b/tools/update_projects_disk_space.php index 517d92dc1d7..f8758232920 100644 --- a/tools/update_projects_disk_space.php +++ b/tools/update_projects_disk_space.php @@ -1,89 +1,95 @@ -database(); +$db = \NDB_Factory::singleton()->database(); $config = \NDB_Config::singleton(); -$dataDir = $config->getSetting('dataDirBasepath'); -$list_of_projects = Utility::getProjectList(); +$dataDir = $config->getSetting('dataDirBasepath'); +$projects = Utility::getProjectList(); -$project_data = []; -// NOTE: Below is EEGNet specific. Other projects may want to use 'files' -// or a different query -foreach ($list_of_projects as $pid => $project) { - $project_directory = $db->pselectOne( +$projectData = []; + +foreach ($projects as $pid => $project) { + $projectDir = $db->pselectOne( "SELECT DISTINCT( SUBSTRING(SUBSTRING_INDEX(FilePath, '/', 2), 1) ) - FROM physiological_file pf - LEFT JOIN session s ON s.ID = pf.SessionID - WHERE s.ProjectID=:PID LIMIT 1;", + FROM ( + SELECT FilePath, SessionID + FROM physiological_file pf + UNION + SELECT File, SessionID as FilePath FROM files f + ) file_table + LEFT JOIN session s ON s.ID = file_table.SessionID + WHERE FilePath LIKE 'bids_imports%' + AND s.ProjectID=:PID LIMIT 1;", ['PID' => $pid] ); - if (str_starts_with($project_directory, 'bids_imports')) { - $full_path = $dataDir . $project_directory; - $dir_size_gb = round( - get_dir_size($full_path) / pow(10, 9), + if (!is_null($projectDir)) { + $fullPath = $dataDir . $projectDir; + $dirSizeGB = round( + getDirSize($fullPath) / pow(10, 9), 1 ); - $project_data[$project]['total'] = $dir_size_gb; + $projectData[$project]['total'] = $dirSizeGB; } } -$cached_data_type_id = $db->pselectOneInt( +$cachedDataTypeID = $db->pselectOneInt( "SELECT `CachedDataTypeID` FROM `cached_data_type` WHERE `Name` = 'projects_disk_space'", [] ); -$row_exists = $db->pselectOne( +$rowExists = $db->pselectOne( "SELECT Value FROM cached_data WHERE CachedDataTypeID = :CDTID;", - ['CDTID' => $cached_data_type_id] + ['CDTID' => $cachedDataTypeID] ); -if ($row_exists) { +if ($rowExists) { $db->update( 'cached_data', - ['Value' => json_encode($project_data)], - ['CachedDataTypeID' => $cached_data_type_id] + ['Value' => json_encode($projectData)], + ['CachedDataTypeID' => $cachedDataTypeID] ); } else { $db->insert( 'cached_data', [ - 'CachedDataTypeID' => $cached_data_type_id, - 'Value' => json_encode($project_data) + 'CachedDataTypeID' => $cachedDataTypeID, + 'Value' => json_encode($projectData) ] ); } -echo "Disk space updated\r\n"; +echo "cached_data:projects_disk_space updated\r\n"; /** * Calculate directory size, recursively, skipping .tgz files * + * @param string $directory Target directory + * * @return int */ -function get_dir_size($directory){ - $size = 0; +function getDirSize(string $directory): int +{ + $size = 0; $files = glob($directory . '/*'); - foreach($files as $path){ + foreach ($files as $path) { if (!str_ends_with($path, '.tgz')) { is_file($path) && $size += filesize($path); - is_dir($path) && $size += get_dir_size($path); + is_dir($path) && $size += getDirSize($path); } } return $size; From 81204c2bf2a1a9da194b730fb35934873047e349 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir <15801528+jeffersoncasimir@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:33:20 -0400 Subject: [PATCH 15/40] Remove commented code --- modules/statistics/jsx/widgets/studyprogression.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 4c9cbe705d2..2b39679f0da 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -248,14 +248,7 @@ const StudyProgression = (props) => { gap: '10px', }} > -
- {/* setShowFiltersBreakdown((prev) => !prev)}*/} - {/*>*/} - {/* {showFiltersBreakdown ? 'Hide Filters' : 'Show Filters'}*/} - {/**/} +
{showFiltersBreakdown && ( Date: Tue, 16 Sep 2025 16:35:39 -0400 Subject: [PATCH 16/40] Fix conflict --- modules/statistics/jsx/widgets/studyprogression.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 2b39679f0da..770c3483b72 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -63,7 +63,7 @@ const StudyProgression = (props) => { titlePrefix: 'Month', }, }, - 'project_sizes': { // This should be a class + 'project_sizes': { // This should be a class 'size_byproject': { sizing: 11, title: 'Dataset size breakdown by project', @@ -248,7 +248,7 @@ const StudyProgression = (props) => { gap: '10px', }} > -
{showFiltersBreakdown && ( Date: Wed, 17 Sep 2025 13:01:23 -0400 Subject: [PATCH 17/40] Compliance changes --- modules/dqt/php/module.class.inc | 6 +++++- modules/statistics/php/widgets.class.inc | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 24b1741a5d5..17af8cd203b 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -108,7 +108,11 @@ class Module extends \Module return [ new \LORIS\dashboard\DataWidget( - "Dataset Size", + new \LORIS\GUI\LocalizableString( + "dqt", + "Dataset Size", + "Dataset Size", + ), $data, "", 'rgb(186,225,255)', diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index 9aa547562e1..3abfedec144 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -210,7 +210,7 @@ class Widgets extends \NDB_Page implements ETagCalculator FROM physiological_file pf JOIN session s ON (s.ID=pf.SessionID) WHERE s.ProjectID IN (" . - join($user->getProjectIDs()) + join(',', $user->getProjectIDs()) . ")", [] ); @@ -241,7 +241,7 @@ class Widgets extends \NDB_Page implements ETagCalculator $values['size_byproject'] = $projectsSizes; $values['eeg_data'] = [ - $eeg_data + 'total_recordings' => $eeg_data ]; $this->_cache = new \LORIS\Http\Response\JsonResponse($values); @@ -638,7 +638,9 @@ class Widgets extends \NDB_Page implements ETagCalculator && isset($value['ProjectID']) && strval($value['ProjectID']) === $projectID ) { - $title = $widgetConfig['ref']->label()->getN($value['count']); + $title = $widgetConfig['ref']->label()->getN( + intval(preg_split('/\s+/', strval($value['count']))[0]) + ); $projectData[] = [ 'title' => $title, From 86c1a8080a1d996cfd6991c11bb265b0e68aed6d Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 17 Sep 2025 13:31:43 -0400 Subject: [PATCH 18/40] Lint fixes --- modules/statistics/php/charts.class.inc | 1 - tools/update_projects_disk_space.php | 1 - 2 files changed, 2 deletions(-) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index a68abd230f4..8c33c61357d 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -843,7 +843,6 @@ class Charts extends \NDB_Page return $data; } - /** * Handle an incoming request for project size breakdown. * diff --git a/tools/update_projects_disk_space.php b/tools/update_projects_disk_space.php index f8758232920..508f430d34d 100644 --- a/tools/update_projects_disk_space.php +++ b/tools/update_projects_disk_space.php @@ -74,7 +74,6 @@ echo "cached_data:projects_disk_space updated\r\n"; - /** * Calculate directory size, recursively, skipping .tgz files * From 30e55924b831c59f767d4406f3860dca7fc50ebc Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 17 Sep 2025 14:21:59 -0400 Subject: [PATCH 19/40] Static fixes --- modules/dqt/php/module.class.inc | 25 ++++---- .../php/module.class.inc | 49 -------------- modules/statistics/php/charts.class.inc | 64 +++---------------- 3 files changed, 24 insertions(+), 114 deletions(-) diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 17af8cd203b..83e94f79923 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -92,18 +92,21 @@ class Module extends \Module ); $data = []; - foreach ($projects as $project) { - $projectName = $project->getName(); - if (!in_array($projectName, array_keys($cachedSizeData))) { - continue; - } - $data[] = [ - 'ProjectID' => $project->getId(), - 'ProjectName' => $projectName, - 'count' => "{$cachedSizeData[$projectName]['total']} GB", - 'url' => "$baseURL/dqt", - ]; + if (!is_null($cachedSizeData)) { + foreach ($projects as $project) { + $projectName = $project->getName(); + if (!in_array($projectName, array_keys($cachedSizeData))) { + continue; + } + + $data[] = [ + 'ProjectID' => $project->getId(), + 'ProjectName' => $projectName, + 'count' => "{$cachedSizeData[$projectName]['total']} GB", + 'url' => "$baseURL/dqt", + ]; + } } return [ diff --git a/modules/electrophysiology_browser/php/module.class.inc b/modules/electrophysiology_browser/php/module.class.inc index bc5ab7d70f1..98c1719eaa7 100644 --- a/modules/electrophysiology_browser/php/module.class.inc +++ b/modules/electrophysiology_browser/php/module.class.inc @@ -63,53 +63,4 @@ class Module extends \Module { return dgettext("electrophysiology_browser", "Electrophysiology Browser"); } - - /** - * {@inheritDoc} - * - * @param string $type The type of widgets to get. - * @param \User $user The user widgets are being retrieved for. - * @param array $options A type dependent list of options to provide - * to the widget. - * - * @return \LORIS\GUI\Widget[] - */ - public function getWidgets(string $type, \User $user, array $options) : array - { - $factory = \NDB_Factory::singleton(); - $baseURL = $factory->settings()->getBaseURL(); - $projects = $user->getProjects(); - - switch ($type) { - case 'study-progression': - $DB = $factory->database(); - $data = $DB->pselectWithIndexKey( - "SELECT - p.ProjectID, - p.Name AS ProjectName, - COUNT(s.ID) AS count, - CONCAT('" - . $baseURL . - "/electrophysiology_browser/?project=', p.Name - ) as url - FROM session s - JOIN Project p ON p.ProjectID = s.ProjectID - JOIN physiological_file pf ON pf.SessionID = s.ID - WHERE s.Active <> 'N' - AND s.CenterID <> 1 - GROUP BY p.Name", - [], - 'ProjectID' - ); - return [ - new \LORIS\dashboard\DataWidget( - "EEG Sessions", - $data, - "", - 'rgb(186,255,201)', - ) - ]; - } - return []; - } } diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 8c33c61357d..395d8cc69ac 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -868,63 +868,19 @@ class Charts extends \NDB_Page ); $projectData = []; - foreach ($projects as $project) { - $projectName = $project->getName(); - if (in_array($projectName, array_keys($cachedSizeData))) { - $projectData[] = [ - 'label' => $projectName, - ...$cachedSizeData[$projectName], - ]; - } - } - - return (new \LORIS\Http\Response\JsonResponse($projectData)); - } - - /** - * Handle an incoming request for EEG recordings by site breakdown. - * - * @return ResponseInterface - */ - private function _handleSiteEEGRecordingsBreakdown() - { - $DB = \NDB_Factory::singleton()->database(); - $user = \NDB_Factory::singleton()->user(); - $sites = $user->getStudySites(); - - $conditions = $this->_buildQueryConditions(); - $user = \NDB_Factory::singleton()->user(); - - $data = $DB->pselect( - "SELECT s.CenterID, - COUNT(distinct s.ID) as count - FROM physiological_file pf - LEFT JOIN session s ON (s.ID=pf.SessionID) - - GROUP BY s.CenterID - ORDER BY s.CenterID", - [] - ); - - $processed_data = []; - foreach ($data as $row) { - $processed_data[$row['CenterID']] = [ - 'CenterID' => $row['CenterID'], - 'Name' => $row['Name'], - 'count' => intval($row['count']), - ]; - } - $siteData = []; - foreach ($sites as $siteID => $siteName) { - if (in_array($siteID, array_keys($processed_data))) { - $siteData[] = [ - 'label' => $siteName, - 'total' => $processed_data[$siteID]['count'], - ]; + if (!is_null($cachedSizeData)) { + foreach ($projects as $project) { + $projectName = $project->getName(); + if (in_array($projectName, array_keys($cachedSizeData))) { + $projectData[] = [ + 'label' => $projectName, + ...$cachedSizeData[$projectName], + ]; + } } } - return (new \LORIS\Http\Response\JsonResponse($siteData)); + return (new \LORIS\Http\Response\JsonResponse($projectData)); } } From d0aa6bb9491e6d65f60f7505180ea4bb39b73ee5 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 17 Sep 2025 15:16:00 -0400 Subject: [PATCH 20/40] Satisfy linter --- modules/dqt/php/module.class.inc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 83e94f79923..269dd0895f4 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -100,10 +100,11 @@ class Module extends \Module continue; } - $data[] = [ + $datasetSize = "{$cachedSizeData[$projectName]['total']} GB"; + $data[] = [ 'ProjectID' => $project->getId(), 'ProjectName' => $projectName, - 'count' => "{$cachedSizeData[$projectName]['total']} GB", + 'count' => $datasetSize, 'url' => "$baseURL/dqt", ]; } From 683709dff15265c358da4b37659b6bc3ef6fe884 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 17 Sep 2025 15:23:39 -0400 Subject: [PATCH 21/40] Satisfy linter --- modules/statistics/jsx/WidgetIndex.js | 12 ++--- modules/statistics/php/charts.class.inc | 65 ++++++++++++++++++++---- modules/statistics/php/widgets.class.inc | 10 ++-- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/modules/statistics/jsx/WidgetIndex.js b/modules/statistics/jsx/WidgetIndex.js index 3839ae04916..01e1e25b2b0 100644 --- a/modules/statistics/jsx/WidgetIndex.js +++ b/modules/statistics/jsx/WidgetIndex.js @@ -34,7 +34,7 @@ const WidgetIndex = (props) => { let {title, chartType, options} = chartDetails[section][chartID]; return (
{/* Chart Title and Toggle */}
@@ -207,9 +207,7 @@ const WidgetIndex = (props) => { } } const queryString = '?' + new URLSearchParams(formObject).toString(); - let newChartDetails = {...clearedChartDetails}; - - const chartPromises = []; + let newChartDetails = {...chartDetails}; Object.keys(chartDetails[section]).forEach( (chart) => { // update filters @@ -228,13 +226,9 @@ const WidgetIndex = (props) => { newChartDetails[section][chart] = data[section][chart]; } ); - chartPromises.push(chartPromise); } ); - - Promise.all(chartPromises).then(() => { - setChartDetails(newChartDetails); - }); + setChartDetails(newChartDetails); }; /** diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 395d8cc69ac..a68abd230f4 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -843,6 +843,7 @@ class Charts extends \NDB_Page return $data; } + /** * Handle an incoming request for project size breakdown. * @@ -868,19 +869,63 @@ class Charts extends \NDB_Page ); $projectData = []; - - if (!is_null($cachedSizeData)) { - foreach ($projects as $project) { - $projectName = $project->getName(); - if (in_array($projectName, array_keys($cachedSizeData))) { - $projectData[] = [ - 'label' => $projectName, - ...$cachedSizeData[$projectName], - ]; - } + foreach ($projects as $project) { + $projectName = $project->getName(); + if (in_array($projectName, array_keys($cachedSizeData))) { + $projectData[] = [ + 'label' => $projectName, + ...$cachedSizeData[$projectName], + ]; } } return (new \LORIS\Http\Response\JsonResponse($projectData)); } + + /** + * Handle an incoming request for EEG recordings by site breakdown. + * + * @return ResponseInterface + */ + private function _handleSiteEEGRecordingsBreakdown() + { + $DB = \NDB_Factory::singleton()->database(); + $user = \NDB_Factory::singleton()->user(); + $sites = $user->getStudySites(); + + $conditions = $this->_buildQueryConditions(); + + $user = \NDB_Factory::singleton()->user(); + + $data = $DB->pselect( + "SELECT s.CenterID, + COUNT(distinct s.ID) as count + FROM physiological_file pf + LEFT JOIN session s ON (s.ID=pf.SessionID) + + GROUP BY s.CenterID + ORDER BY s.CenterID", + [] + ); + + $processed_data = []; + foreach ($data as $row) { + $processed_data[$row['CenterID']] = [ + 'CenterID' => $row['CenterID'], + 'Name' => $row['Name'], + 'count' => intval($row['count']), + ]; + } + $siteData = []; + foreach ($sites as $siteID => $siteName) { + if (in_array($siteID, array_keys($processed_data))) { + $siteData[] = [ + 'label' => $siteName, + 'total' => $processed_data[$siteID]['count'], + ]; + } + } + + return (new \LORIS\Http\Response\JsonResponse($siteData)); + } } diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index 3abfedec144..ab86e190511 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -191,10 +191,12 @@ class Widgets extends \NDB_Page implements ETagCalculator } } - if (in_array($projectName, array_keys($cachedSizeData))) { - $projectSize = $cachedSizeData[$projectName]['total']; - $projectsSizes[$projectName] = $projectSize; - $totalSizeOfProjectsGB += floatval($projectSize); + if (!is_null($cachedSizeData)) { + if (in_array($projectName, array_keys($cachedSizeData))) { + $projectSize = $cachedSizeData[$projectName]['total']; + $projectsSizes[$projectName] = $projectSize; + $totalSizeOfProjectsGB += floatval($projectSize); + } } } From 08f6a540f819d44f53b7a3ddf0a65a0906498898 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 20 Oct 2025 12:24:54 -0400 Subject: [PATCH 22/40] Corrected merge change --- modules/statistics/jsx/widgets/studyprogression.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 770c3483b72..92a10230645 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -197,6 +197,8 @@ const StudyProgression = (props) => {

There have been no scans yet.

), title: title('Site Scans'), + subtitle: 'Total scans: ' + + json['studyprogression']['total_scans'], onToggleFilters: () => setShowFiltersScans((prev) => !prev), }, { From d3ce416dd9dcd5442d35ee40c52b0ecba27b6a35 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 20 Oct 2025 17:26:34 -0400 Subject: [PATCH 23/40] Remove console.log --- modules/statistics/jsx/widgets/studyprogression.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 92a10230645..40423b3c114 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -101,7 +101,6 @@ const StudyProgression = (props) => { setChartDetails(data); }); json = props.data; - console.log('thejson', json); setLoading(false); } }, [props.data, t]); From eed888ba21d1f67ead3ede1832d1b322712192c9 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 27 Oct 2025 16:37:25 -0400 Subject: [PATCH 24/40] Rebase to aces/main --- jsx/Panel.js | 1 + modules/behavioural_qc/php/module.class.inc | 10 +++++----- modules/candidate_list/php/module.class.inc | 10 +++++----- modules/imaging_browser/php/module.class.inc | 6 +++--- modules/statistics/css/WidgetIndex.css | 2 +- modules/statistics/css/recruitment.css | 2 +- modules/statistics/jsx/WidgetIndex.js | 12 +++++++++--- .../jsx/widgets/helpers/chartBuilder.js | 2 -- .../jsx/widgets/helpers/queryChartForm.js | 15 --------------- modules/statistics/jsx/widgets/recruitment.js | 10 +++++----- .../statistics/jsx/widgets/studyprogression.js | 7 +++---- modules/statistics/php/charts.class.inc | 13 ++++--------- raisinbread/RB_files/RB_candidate.sql | 2 +- 13 files changed, 38 insertions(+), 54 deletions(-) diff --git a/jsx/Panel.js b/jsx/Panel.js index 6fc260c57c2..0fca9987bba 100644 --- a/jsx/Panel.js +++ b/jsx/Panel.js @@ -153,6 +153,7 @@ Panel.propTypes = { bold: PropTypes.bool, panelSize: PropTypes.string, style: PropTypes.object, + children: PropTypes.node, }; Panel.defaultProps = { initCollapsed: false, diff --git a/modules/behavioural_qc/php/module.class.inc b/modules/behavioural_qc/php/module.class.inc index 0792d17fae5..ed877b42bb6 100644 --- a/modules/behavioural_qc/php/module.class.inc +++ b/modules/behavioural_qc/php/module.class.inc @@ -78,18 +78,18 @@ class Module extends \Module case 'study-progression': $DB = $factory->database(); $data = $DB->pselectWithIndexKey( - "SELECT + "SELECT p.ProjectID, COUNT(*) AS count, CONCAT('$baseURL/behavioural_qc/?Project=', p.ProjectID) AS url, p.Name AS ProjectName - FROM flag f - JOIN session s ON f.SessionID = s.ID + FROM flag f + JOIN session s ON f.SessionID = s.ID JOIN Project p ON p.ProjectID = s.ProjectID WHERE DataID IS NOT NULL - AND s.Active <> 'N' - AND s.CenterID <> 1 + AND s.Active <> 'N' + AND s.CenterID <> 1 AND f.CommentID NOT LIKE 'DDE_%' GROUP BY p.Name", [], diff --git a/modules/candidate_list/php/module.class.inc b/modules/candidate_list/php/module.class.inc index 15821f713dd..f83c12db85f 100644 --- a/modules/candidate_list/php/module.class.inc +++ b/modules/candidate_list/php/module.class.inc @@ -121,16 +121,16 @@ class Module extends \Module AS url, ProjectName FROM ( - SELECT + SELECT c.PSCID, COALESCE(p.ProjectID, p2.ProjectID) AS ProjectID, COALESCE(p.Name, p2.Name) AS ProjectName FROM candidate c - LEFT JOIN session s ON s.CandidateID = c.ID + LEFT JOIN session s ON s.CandidateID = c.ID LEFT JOIN Project p ON p.ProjectID = s.ProjectID - JOIN Project p2 ON c.RegistrationProjectID = p2.ProjectID - WHERE c.Active <> 'N' - AND s.Active <> 'N' + JOIN Project p2 ON c.RegistrationProjectID = p2.ProjectID + WHERE c.Active <> 'N' + AND s.Active <> 'N' AND s.CenterID <> 1 ) AS sub GROUP BY ProjectID, ProjectName;", diff --git a/modules/imaging_browser/php/module.class.inc b/modules/imaging_browser/php/module.class.inc index 4e577d48445..bd848130929 100644 --- a/modules/imaging_browser/php/module.class.inc +++ b/modules/imaging_browser/php/module.class.inc @@ -129,15 +129,15 @@ class Module extends \Module case 'study-progression': $DB = $factory->database(); $data = $DB->pselectWithIndexKey( - "SELECT + "SELECT p.ProjectID, p.Name AS ProjectName, COUNT(s.ID) AS count, - CONCAT('".$baseURL."/imaging_browser/?project=', p.Name) as url + '".$baseURL."/imaging_browser' as url FROM session s JOIN Project p ON p.ProjectID = s.ProjectID JOIN mri_upload mu ON mu.SessionID = s.ID - WHERE s.Active <> 'N' + WHERE s.Active <> 'N' AND s.CenterID <> 1 GROUP BY p.Name", [], diff --git a/modules/statistics/css/WidgetIndex.css b/modules/statistics/css/WidgetIndex.css index ff68a6b6c96..eb3179bc695 100644 --- a/modules/statistics/css/WidgetIndex.css +++ b/modules/statistics/css/WidgetIndex.css @@ -206,4 +206,4 @@ .filter-grid { grid-template-columns: 1fr; } -} +} \ No newline at end of file diff --git a/modules/statistics/css/recruitment.css b/modules/statistics/css/recruitment.css index 8825797b107..1aaccf5781d 100644 --- a/modules/statistics/css/recruitment.css +++ b/modules/statistics/css/recruitment.css @@ -42,4 +42,4 @@ transform: translateY(-10px); background-color: #f9fdff; box-shadow: 0 12px 12px rgba(0, 0, 0, 0.25); -} +} \ No newline at end of file diff --git a/modules/statistics/jsx/WidgetIndex.js b/modules/statistics/jsx/WidgetIndex.js index 01e1e25b2b0..3839ae04916 100644 --- a/modules/statistics/jsx/WidgetIndex.js +++ b/modules/statistics/jsx/WidgetIndex.js @@ -34,7 +34,7 @@ const WidgetIndex = (props) => { let {title, chartType, options} = chartDetails[section][chartID]; return (
{/* Chart Title and Toggle */}
@@ -207,7 +207,9 @@ const WidgetIndex = (props) => { } } const queryString = '?' + new URLSearchParams(formObject).toString(); - let newChartDetails = {...chartDetails}; + let newChartDetails = {...clearedChartDetails}; + + const chartPromises = []; Object.keys(chartDetails[section]).forEach( (chart) => { // update filters @@ -226,9 +228,13 @@ const WidgetIndex = (props) => { newChartDetails[section][chart] = data[section][chart]; } ); + chartPromises.push(chartPromise); } ); - setChartDetails(newChartDetails); + + Promise.all(chartPromises).then(() => { + setChartDetails(newChartDetails); + }); }; /** diff --git a/modules/statistics/jsx/widgets/helpers/chartBuilder.js b/modules/statistics/jsx/widgets/helpers/chartBuilder.js index c5a7b7dda92..3b979a5576c 100644 --- a/modules/statistics/jsx/widgets/helpers/chartBuilder.js +++ b/modules/statistics/jsx/widgets/helpers/chartBuilder.js @@ -236,10 +236,8 @@ const createLineChart = (data, columns, id, label, targetModal, titlePrefix) => name = nameFormat(d[i].name); value = valueFormat(d[i].value, d[i].ratio, d[i].id, d[i].index); - // Calculate percentage based on grand total of entire dataset let percentage = grandTotal > 0 ? ((d[i].value / grandTotal) * 100).toFixed(1) : 0; - bgcolor = $$.levelColor ? $$.levelColor(d[i].value) : color(d[i].id); text += ""; diff --git a/modules/statistics/jsx/widgets/helpers/queryChartForm.js b/modules/statistics/jsx/widgets/helpers/queryChartForm.js index 2f58aa24a33..9d09cb0b6c1 100644 --- a/modules/statistics/jsx/widgets/helpers/queryChartForm.js +++ b/modules/statistics/jsx/widgets/helpers/queryChartForm.js @@ -54,21 +54,6 @@ const QueryChartForm = (props) => { } } - setFormDataObj((prevState) => { - const newFormData = { - ...prevState, - [formElement]: normalizedValue, - }; - if ( - (normalizedValue !== undefined - || prevState[formElement] !== undefined) - && !(formElement.includes('date') && value < '1900-01-01') - ) { - props.callback(newFormData); - } - return newFormData; - }); - const newFormData = { ...formDataObj, [formElement]: normalizedValue, diff --git a/modules/statistics/jsx/widgets/recruitment.js b/modules/statistics/jsx/widgets/recruitment.js index 423b02bb25e..77348e35313 100644 --- a/modules/statistics/jsx/widgets/recruitment.js +++ b/modules/statistics/jsx/widgets/recruitment.js @@ -32,7 +32,7 @@ const Recruitment = (props) => { dataType: 'pie', label: 'Age (Years)', options: {pie: 'pie', bar: 'bar'}, - yLabel: 'Candidates registered', + yLabel: t('Candidates registered', {ns: 'statistics'}), legend: 'under', chartObject: null, }, @@ -43,7 +43,7 @@ const Recruitment = (props) => { dataType: 'pie', label: 'Ethnicity', options: {pie: 'pie', bar: 'bar'}, - yLabel: 'Candidates registered', + yLabel: t('Candidates registered', {ns: 'statistics'}), legend: 'under', chartObject: null, }, @@ -57,7 +57,7 @@ const Recruitment = (props) => { label: 'Participants', legend: '', options: {pie: 'pie', bar: 'bar'}, - yLabel: 'Candidates registered', + yLabel: t('Candidates registered', {ns: 'statistics'}), chartObject: null, }, 'siterecruitment_bysex': { @@ -67,7 +67,7 @@ const Recruitment = (props) => { dataType: 'bar', legend: 'under', options: {bar: 'bar', pie: 'pie'}, - yLabel: 'Candidates registered', + yLabel: t('Candidates registered', {ns: 'statistics'}), chartObject: null, }, }, @@ -79,7 +79,7 @@ const Recruitment = (props) => { dataType: 'line', legend: '', options: {line: 'line'}, - yLabel: 'Candidates registered', + yLabel: t('Candidates registered', {ns: 'statistics'}), chartObject: null, }, }, diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 40423b3c114..662e8acde41 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -45,7 +45,7 @@ const StudyProgression = (props) => { legend: 'under', options: {line: 'line'}, chartObject: null, - yLabel: 'Candidates registered', + yLabel: t('Candidates registered', {ns: 'statistics'}), titlePrefix: 'Month', }, }, @@ -59,7 +59,7 @@ const StudyProgression = (props) => { legend: '', options: {line: 'line'}, chartObject: null, - yLabel: 'Candidates registered', + yLabel: t('Candidates registered', {ns: 'statistics'}), titlePrefix: 'Month', }, }, @@ -76,7 +76,7 @@ const StudyProgression = (props) => { legend: '', options: {pie: 'pie', bar: 'bar'}, chartObject: null, - yLabel: 'Size (GB)', + yLabel: t('Size (GB)', {ns: 'statistics'}), titlePrefix: 'Project', }, }, @@ -115,7 +115,6 @@ const StudyProgression = (props) => { const filterLabel = (hide) => hide ? t('Hide Filters', {ns: 'loris'}) : t('Show Filters', {ns: 'loris'}); - return loading ? : ( <> Date: Mon, 27 Oct 2025 17:22:38 -0400 Subject: [PATCH 25/40] Fix failing static tests --- modules/statistics/php/charts.class.inc | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index e641fe23219..3cb032d190b 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -867,10 +867,12 @@ class Charts extends \NDB_Page foreach ($projects as $project) { $projectName = $project->getName(); if (in_array($projectName, array_keys($cachedSizeData))) { - $projectData[] = [ - 'label' => $projectName, - ...$cachedSizeData[$projectName], - ]; + if (!is_null($cachedSizeData[$projectName])) { + $projectData[] = [ + 'label' => $projectName, + ...$cachedSizeData[$projectName], + ]; + } } } @@ -888,10 +890,6 @@ class Charts extends \NDB_Page $user = \NDB_Factory::singleton()->user(); $sites = $user->getStudySites(); - $conditions = $this->_buildQueryConditions(); - - $user = \NDB_Factory::singleton()->user(); - $data = $DB->pselect( "SELECT s.CenterID, COUNT(distinct s.ID) as count From f14630cb3d1c81f276ef6d13a7b5a8ebe82da9f8 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 27 Oct 2025 17:35:12 -0400 Subject: [PATCH 26/40] Fix failing static tests --- modules/statistics/php/charts.class.inc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 3cb032d190b..1bc9ab53458 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -864,10 +864,10 @@ class Charts extends \NDB_Page ); $projectData = []; - foreach ($projects as $project) { - $projectName = $project->getName(); - if (in_array($projectName, array_keys($cachedSizeData))) { - if (!is_null($cachedSizeData[$projectName])) { + if (!is_null($cachedSizeData)) { + foreach ($projects as $project) { + $projectName = $project->getName(); + if (in_array($projectName, array_keys($cachedSizeData))) { $projectData[] = [ 'label' => $projectName, ...$cachedSizeData[$projectName], From cf24d8ca2d2ed34991545daa71895ac13c3b7644 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 27 Oct 2025 18:31:27 -0400 Subject: [PATCH 27/40] Fix null scenario --- modules/dqt/php/module.class.inc | 2 +- modules/statistics/php/widgets.class.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 269dd0895f4..690ec6c3d58 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -86,7 +86,7 @@ class Module extends \Module JOIN cached_data_type USING (CachedDataTypeID) WHERE Name='projects_disk_space'", [] - ) + ) ?? '' ), true ); diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index ab86e190511..37646f03066 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -129,7 +129,7 @@ class Widgets extends \NDB_Page implements ETagCalculator JOIN cached_data_type USING (CachedDataTypeID) WHERE Name='projects_disk_space'", [] - ) + ) ?? '' ), true ); From f3e66dc1150ff2f8577c072e4c360915cdb23295 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 27 Oct 2025 18:33:42 -0400 Subject: [PATCH 28/40] Fix null scenario --- modules/statistics/php/charts.class.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 1bc9ab53458..b3b91561e8a 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -858,7 +858,7 @@ class Charts extends \NDB_Page JOIN cached_data_type USING (CachedDataTypeID) WHERE Name='projects_disk_space'", [] - ) + ) ?? '' ), true ); From 15842fab2b9f4452521ef90e4d0219d55391190a Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 29 Oct 2025 12:41:56 -0400 Subject: [PATCH 29/40] Make strings translatable --- modules/dqt/php/module.class.inc | 67 ------------------- .../jsx/widgets/studyprogression.js | 46 ++++++++----- modules/statistics/php/module.class.inc | 56 +++++++++++++++- 3 files changed, 83 insertions(+), 86 deletions(-) diff --git a/modules/dqt/php/module.class.inc b/modules/dqt/php/module.class.inc index 690ec6c3d58..2e00f6a4021 100644 --- a/modules/dqt/php/module.class.inc +++ b/modules/dqt/php/module.class.inc @@ -58,71 +58,4 @@ class Module extends \Module { return dgettext("dqt", "Data Query Tool"); } - - /** - * {@inheritDoc} - * - * @param string $type The type of widgets to get. - * @param \User $user The user widgets are being retrieved for. - * @param array $options A type dependent list of options to provide - * to the widget. - * - * @return \LORIS\GUI\Widget[] - */ - public function getWidgets(string $type, \User $user, array $options) : array - { - $factory = \NDB_Factory::singleton(); - $baseURL = $factory->settings()->getBaseURL(); - $projects = $user->getProjects(); - - switch ($type) { - case 'study-progression': - $DB = $factory->database(); - $cachedSizeData = json_decode( - html_entity_decode( - $DB->pselectOne( - "SELECT Value - FROM cached_data - JOIN cached_data_type USING (CachedDataTypeID) - WHERE Name='projects_disk_space'", - [] - ) ?? '' - ), - true - ); - - $data = []; - - if (!is_null($cachedSizeData)) { - foreach ($projects as $project) { - $projectName = $project->getName(); - if (!in_array($projectName, array_keys($cachedSizeData))) { - continue; - } - - $datasetSize = "{$cachedSizeData[$projectName]['total']} GB"; - $data[] = [ - 'ProjectID' => $project->getId(), - 'ProjectName' => $projectName, - 'count' => $datasetSize, - 'url' => "$baseURL/dqt", - ]; - } - } - - return [ - new \LORIS\dashboard\DataWidget( - new \LORIS\GUI\LocalizableString( - "dqt", - "Dataset Size", - "Dataset Size", - ), - $data, - "", - 'rgb(186,225,255)', - ) - ]; - } - return []; - } } diff --git a/modules/statistics/jsx/widgets/studyprogression.js b/modules/statistics/jsx/widgets/studyprogression.js index 662e8acde41..32df8255495 100644 --- a/modules/statistics/jsx/widgets/studyprogression.js +++ b/modules/statistics/jsx/widgets/studyprogression.js @@ -46,7 +46,7 @@ const StudyProgression = (props) => { options: {line: 'line'}, chartObject: null, yLabel: t('Candidates registered', {ns: 'statistics'}), - titlePrefix: 'Month', + titlePrefix: t('Month', {ns: 'loris'}), }, }, 'total_recruitment': { @@ -60,24 +60,24 @@ const StudyProgression = (props) => { options: {line: 'line'}, chartObject: null, yLabel: t('Candidates registered', {ns: 'statistics'}), - titlePrefix: 'Month', + titlePrefix: t('Month', {ns: 'loris'}), }, }, - 'project_sizes': { // This should be a class + 'project_sizes': { 'size_byproject': { sizing: 11, - title: 'Dataset size breakdown by project', + title: t('Dataset size breakdown by project', {ns: 'statistics'}), filters: '', chartType: 'pie', dataType: 'pie', - label: 'Size (GB)', - units: 'GB', + label: t('Size (GB)', {ns: 'statistics'}), + units: t('GB', {ns: 'loris'}), showPieLabelRatio: false, legend: '', options: {pie: 'pie', bar: 'bar'}, chartObject: null, yLabel: t('Size (GB)', {ns: 'statistics'}), - titlePrefix: 'Project', + titlePrefix: t('Project', {ns: 'loris'}), }, }, }); @@ -192,11 +192,16 @@ const StudyProgression = (props) => { {showChart('total_scans', 'scans_bymonth')}
) : ( -

There have been no scans yet.

+

{t('There have been no scans yet.', {ns: 'statistics'})}

), title: title('Site Scans'), - subtitle: 'Total scans: ' - + json['studyprogression']['total_scans'], + subtitle: t( + 'Total Scans: {{count}}', + { + ns: 'statistics', + count: json['studyprogression']['total_scans'], + } + ), onToggleFilters: () => setShowFiltersScans((prev) => !prev), }, { @@ -233,7 +238,12 @@ const StudyProgression = (props) => { {showChart('total_recruitment', 'siterecruitment_bymonth')}
) : ( -

There have been no candidates registered yet.

+

+ {t( + 'There have been no candidates registered yet.', + {ns: 'statistics'} + )} +

), title: title('Site Recruitment'), onToggleFilters: () => showFiltersBreakdown((prev) => !prev), @@ -264,12 +274,16 @@ const StudyProgression = (props) => { {showChart('project_sizes', 'size_byproject')}
) : ( -

There is no data yet.

+

{t('There is no data yet.', {ns: 'statistics'})}

), - title: 'Study Progression - project dataset sizes', - subtitle: 'Total size: ' - + (json['studyprogression']['total_size'] ?? -1) - + ' GB', + title: title('Project Dataset Sizes'), + subtitle: t( + 'Total Size: {{count}} GB', + { + ns: 'statistics', + count: json['studyprogression']['total_size'] ?? -1, + } + ), }, ]} /> diff --git a/modules/statistics/php/module.class.inc b/modules/statistics/php/module.class.inc index ef2b360998b..67942806db7 100644 --- a/modules/statistics/php/module.class.inc +++ b/modules/statistics/php/module.class.inc @@ -71,11 +71,11 @@ class Module extends \Module */ public function getWidgets(string $type, \User $user, array $options) : array { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); switch ($type) { case 'dashboard': - $factory = \NDB_Factory::singleton(); - $baseURL = $factory->settings()->getBaseURL(); - $widget = new \LORIS\dashboard\Widget( + $widget = new \LORIS\dashboard\Widget( new \LORIS\dashboard\WidgetContent( '', '', @@ -100,6 +100,56 @@ class Module extends \Module $widget->setTemplateVariables(['id' => 'statistics_widgets']); return [$widget]; + case 'study-progression': + $DB = $factory->database(); + $cachedSizeData = json_decode( + html_entity_decode( + $DB->pselectOne( + "SELECT Value + FROM cached_data + JOIN cached_data_type USING (CachedDataTypeID) + WHERE Name='projects_disk_space'", + [] + ) ?? '' + ), + true + ); + + $data = []; + + $projects = $user->getProjects(); + if (!is_null($cachedSizeData)) { + foreach ($projects as $project) { + $projectName = $project->getName(); + if (!in_array($projectName, array_keys($cachedSizeData))) { + continue; + } + + $datasetSize = sprintf( + dgettext('statistics', '%s GB'), + $cachedSizeData[$projectName]['total'] + ); + $data[] = [ + 'ProjectID' => $project->getId(), + 'ProjectName' => $projectName, + 'count' => $datasetSize, + 'url' => "$baseURL/dqt", + ]; + } + } + + return [ + new \LORIS\dashboard\DataWidget( + new \LORIS\GUI\LocalizableString( + "dqt", + "Dataset Size", + "Dataset Size", + ), + $data, + "", + 'rgb(186,225,255)', + ) + ]; } return []; } From 79748e2ef731bbb802f13f0902107f667a282cdf Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 17 Sep 2025 18:40:47 -0400 Subject: [PATCH 30/40] EEG widget + charts endpoints --- .../php/module.class.inc | 54 ++++++++ modules/statistics/css/recruitment.css | 4 +- modules/statistics/php/charts.class.inc | 126 +++++++++++++++++- 3 files changed, 181 insertions(+), 3 deletions(-) diff --git a/modules/electrophysiology_browser/php/module.class.inc b/modules/electrophysiology_browser/php/module.class.inc index 98c1719eaa7..da7fad5e361 100644 --- a/modules/electrophysiology_browser/php/module.class.inc +++ b/modules/electrophysiology_browser/php/module.class.inc @@ -63,4 +63,58 @@ class Module extends \Module { return dgettext("electrophysiology_browser", "Electrophysiology Browser"); } + + + /** + * {@inheritDoc} + * + * @param string $type The type of widgets to get. + * @param \User $user The user widgets are being retrieved for. + * @param array $options A type dependent list of options to provide + * to the widget. + * + * @return \LORIS\GUI\Widget[] + */ + public function getWidgets(string $type, \User $user, array $options) : array + { + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); + $projects = $user->getProjects(); + + switch ($type) { + case 'study-progression': + $DB = $factory->database(); + $data = $DB->pselectWithIndexKey( + "SELECT + p.ProjectID, + p.Name AS ProjectName, + COUNT(s.ID) AS count, + CONCAT('" + . $baseURL . + "/electrophysiology_browser/?project=', p.Name + ) as url + FROM session s + JOIN Project p ON p.ProjectID = s.ProjectID + JOIN physiological_file pf ON pf.SessionID = s.ID + WHERE s.Active <> 'N' + AND s.CenterID <> 1 + GROUP BY p.Name", + [], + 'ProjectID' + ); + return [ + new \LORIS\dashboard\DataWidget( + new \LORIS\GUI\LocalizableString( + "dqt", + "EEG Session", + "EEG Sessions", + ), + $data, + "", + 'rgb(186,255,201)', + ) + ]; + } + return []; + } } diff --git a/modules/statistics/css/recruitment.css b/modules/statistics/css/recruitment.css index 1aaccf5781d..b7c535030dc 100644 --- a/modules/statistics/css/recruitment.css +++ b/modules/statistics/css/recruitment.css @@ -11,7 +11,7 @@ background-color: #2FA4E7; } -.study-progression-container { +.study-progression-container, .eeg-data-container { max-height: 415px; overflow-y: auto; } @@ -42,4 +42,4 @@ transform: translateY(-10px); background-color: #f9fdff; box-shadow: 0 12px 12px rgba(0, 0, 0, 0.25); -} \ No newline at end of file +} diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index b3b91561e8a..7318f574008 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -105,7 +105,9 @@ class Charts extends \NDB_Page case 'agedistribution_line': return $this->_handleAgeDistributionByProject($request); case 'eeg_recordings_by_site': - return $this->_handleSiteEEGRecordingsBreakdown(); + return $this->_handleSiteEEGRecordingsBreakdown($request); + case 'eeg_recordings_by_project': + return $this->_handleProjectEEGRecordingsBreakdown($request); default: return new \LORIS\Http\Response\JSON\NotFound(); } @@ -875,6 +877,128 @@ class Charts extends \NDB_Page } } } + return (new \LORIS\Http\Response\JsonResponse($projectData)); + } + + /** + * Handle an incoming request for EEG recordings by site breakdown. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface + */ + private function _handleSiteEEGRecordingsBreakdown( + ServerRequestInterface $request + ) { + $DB = \NDB_Factory::singleton()->database(); + $user = \NDB_Factory::singleton()->user(); + $sites = $user->getSites(); + + $conditions = $this->_buildQueryConditions(); + + $data = iterator_to_array( + $DB->pselect( + "SELECT + s.CenterID, + psc.Name, + COUNT(distinct s.ID) as count + FROM physiological_file pf + JOIN session s ON (s.ID=pf.SessionID) + JOIN candidate c ON (c.ID=s.CandidateID) + JOIN psc USING (CenterID) + {$conditions['participantStatusJoin']} + WHERE 1 = 1 + {$conditions['projectQuery']} + {$conditions['cohortQuery']} + {$conditions['visitQuery']} + {$conditions['siteQuery']} + {$conditions['participantStatusQuery']} + GROUP BY s.CenterID + ORDER BY s.CenterID", + [] + ) + ); + + $processedData = []; + foreach ($data as $row) { + $processedData[$row['CenterID']] = [ + 'CenterID' => $row['CenterID'], + 'Name' => $row['Name'], + 'count' => intval($row['count']), + ]; + } + + $siteData = []; + foreach ($sites as $siteID => $siteName) { + if (in_array($siteID, array_keys($processedData))) { + $siteData[] = [ + 'label' => $siteName, + 'total' => $processedData[$siteID]['count'], + ]; + } + } + + return (new \LORIS\Http\Response\JsonResponse($siteData)); + } + + /** + * Handle an incoming request for EEG recordings by project breakdown. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface + */ + private function _handleProjectEEGRecordingsBreakdown( + ServerRequestInterface $request + ) { + $DB = \NDB_Factory::singleton()->database(); + $user = \NDB_Factory::singleton()->user(); + $projects = $user->getProjects(); + + $conditions = $this->_buildQueryConditions(); + + $data = iterator_to_array( + $DB->pselect( + "SELECT + s.ProjectID, + p.Name, + COUNT(distinct s.ID) as count + FROM physiological_file pf + JOIN session s ON (s.ID=pf.SessionID) + JOIN Project p USING (ProjectID) + JOIN candidate c ON (c.ID=s.CandidateID) + JOIN psc ON (s.CenterID = psc.CenterID) + {$conditions['participantStatusJoin']} + WHERE 1 = 1 + {$conditions['projectQuery']} + {$conditions['cohortQuery']} + {$conditions['visitQuery']} + {$conditions['siteQuery']} + {$conditions['participantStatusQuery']} + GROUP BY s.CenterID + ORDER BY s.CenterID", + [] + ) + ); + + $processedData = []; + foreach ($data as $row) { + $processedData[$row['ProjectID']] = [ + 'ProjectID' => $row['ProjectID'], + 'Name' => $row['Name'], + 'count' => intval($row['count']), + ]; + } + + $projectData = []; + foreach ($projects as $projectID => $projectName) { + if (in_array($projectID, array_keys($processedData))) { + $projectData[] = [ + 'label' => $projectName, + 'total' => $processedData[$projectID]['count'], + ]; + } + } return (new \LORIS\Http\Response\JsonResponse($projectData)); } From 94e0088c92d8c8fd856ff7f9b51fc680898db1de Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 29 Sep 2025 09:22:34 -0400 Subject: [PATCH 31/40] Show graphs + Fix payload --- modules/statistics/jsx/WidgetIndex.js | 10 + .../jsx/widgets/electrophysiology.js | 189 ++++++++++++++++++ modules/statistics/php/charts.class.inc | 4 +- 3 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 modules/statistics/jsx/widgets/electrophysiology.js diff --git a/modules/statistics/jsx/WidgetIndex.js b/modules/statistics/jsx/WidgetIndex.js index 3839ae04916..fdc9f7d33c4 100644 --- a/modules/statistics/jsx/WidgetIndex.js +++ b/modules/statistics/jsx/WidgetIndex.js @@ -3,6 +3,7 @@ import React, {useEffect, useState} from 'react'; import PropTypes from 'prop-types'; import Recruitment from './widgets/recruitment'; import StudyProgression from './widgets/studyprogression'; +import Electrophysiology from './widgets/electrophysiology'; import {fetchData} from './Fetch'; import Modal from 'Modal'; import Loader from 'Loader'; @@ -23,6 +24,7 @@ import jaStrings from '../locale/ja/LC_MESSAGES/statistics.json'; const WidgetIndex = (props) => { const [recruitmentData, setRecruitmentData] = useState({}); const [studyProgressionData, setStudyProgressionData] = useState({}); + const [electrophysiologyData, setElectrophysiologyData] = useState({}); const [modalChart, setModalChart] = useState(null); const {t, i18n} = useTranslation(); useEffect( () => { @@ -253,6 +255,8 @@ const WidgetIndex = (props) => { ); setRecruitmentData(data); setStudyProgressionData(data); + setElectrophysiologyData(data); + }; setup().catch( (error) => { @@ -348,6 +352,12 @@ const WidgetIndex = (props) => { showChart ={showChart} updateFilters ={updateFilters} /> + ); }; diff --git a/modules/statistics/jsx/widgets/electrophysiology.js b/modules/statistics/jsx/widgets/electrophysiology.js new file mode 100644 index 00000000000..1b9fe27c2ec --- /dev/null +++ b/modules/statistics/jsx/widgets/electrophysiology.js @@ -0,0 +1,189 @@ +import React, {useEffect, useState} from 'react'; +import PropTypes from 'prop-types'; +import Loader from 'Loader'; +import Panel from 'Panel'; +import {QueryChartForm} from './helpers/queryChartForm'; +import {setupCharts} from './helpers/chartBuilder'; + +/** + * Electrophysiology - a widget containing statistics for EEG data. + * + * @param {object} props + * @return {JSX.Element} + */ +const Electrophysiology = (props) => { + const [loading, setLoading] = useState(true); + const [showFiltersBreakdown, setShowFiltersBreakdown] = useState(false); + + let json = props.data; + + const [chartDetails, setChartDetails] = useState({ + 'site_recordings': { + 'eeg_recordings_by_site': { + sizing: 11, + title: 'EEG Recordings by site', + filters: '', + chartType: 'pie', + dataType: 'pie', + label: 'EEG Recordings', + units: null, + showPieLabelRatio: true, + legend: '', + options: {pie: 'pie', bar: 'bar'}, + chartObject: null, + yLabel: 'EEG Recordings', + titlePrefix: 'Site', + }, + }, + 'project_recordings': { + 'eeg_recordings_by_project': { + sizing: 11, + title: 'EEG Recordings by project', + filters: '', + chartType: 'pie', + dataType: 'pie', + label: 'EEG Recordings', + units: null, + showPieLabelRatio: true, + legend: '', + options: {pie: 'pie', bar: 'bar'}, + chartObject: null, + yLabel: 'EEG Recordings', + titlePrefix: 'Project', + }, + }, + }); + + const showChart = ((section, chartID) => { + return props.showChart(section, chartID, + chartDetails, setChartDetails); + }); + + /** + * useEffect - modified to run when props.data updates. + */ + useEffect(() => { + if (json && Object.keys(json).length !== 0) { + setupCharts(false, chartDetails).then((data) => { + setChartDetails(data); + }); + json = props.data; + console.log('eeg_json', json); + setLoading(false); + } + }, [props.data]); + + const updateFilters = (formDataObj, section) => { + props.updateFilters(formDataObj, section, + chartDetails, setChartDetails); + }; + + // Helper function to calculate total recruitment + const getTotalRecordings = () => { + return json['eeg_data']['total_recordings'] || -1; + }; + + return loading ? : ( + <> + { + setupCharts(false, chartDetails); + + // reset filters when switching views + setShowFiltersBreakdown(false); + }} + views={[ + { + content: + getTotalRecordings() > 0 ? ( +
+
+ +
+ {showFiltersBreakdown && ( + { + updateFilters(formDataObj, 'project_recordings'); + }} + /> + )} + {showChart('project_recordings', 'eeg_recordings_by_project')} +
+ ) : ( +

There is no data yet.

+ ), + title: 'EEG data - recordings by project', + subtitle: 'Total recordings: ' + getTotalRecordings(), + }, + { + content: + getTotalRecordings() > 0 ? ( +
+
+ +
+ {showFiltersBreakdown && ( + { + updateFilters(formDataObj, 'site_recordings'); + }} + /> + )} + {showChart('site_recordings', 'eeg_recordings_by_site')} +
+ ) : ( +

There is no data yet.

+ ), + title: 'EEG data - recordings by site', + subtitle: 'Total recordings: ' + getTotalRecordings(), + }, + ]} + /> + + ); +}; +Electrophysiology.propTypes = { + data: PropTypes.object, + baseURL: PropTypes.string, + updateFilters: PropTypes.func, + showChart: PropTypes.func, +}; +Electrophysiology.defaultProps = { + data: {}, +}; + +export default Electrophysiology; diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 7318f574008..b9ae65b20e0 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -929,10 +929,10 @@ class Charts extends \NDB_Page } $siteData = []; - foreach ($sites as $siteID => $siteName) { + foreach ($sites as $siteID => $site) { if (in_array($siteID, array_keys($processedData))) { $siteData[] = [ - 'label' => $siteName, + 'label' => $site->getCenterName(), 'total' => $processedData[$siteID]['count'], ]; } From 259eb1c78dce3d0420d6f8b9e1cde6f0aeaf1450 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Mon, 29 Sep 2025 09:29:23 -0400 Subject: [PATCH 32/40] php lint fix --- modules/electrophysiology_browser/php/module.class.inc | 1 - modules/statistics/php/charts.class.inc | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/electrophysiology_browser/php/module.class.inc b/modules/electrophysiology_browser/php/module.class.inc index da7fad5e361..2b25c283e3a 100644 --- a/modules/electrophysiology_browser/php/module.class.inc +++ b/modules/electrophysiology_browser/php/module.class.inc @@ -63,7 +63,6 @@ class Module extends \Module { return dgettext("electrophysiology_browser", "Electrophysiology Browser"); } - /** * {@inheritDoc} diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index b9ae65b20e0..3d6a397f346 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -890,9 +890,9 @@ class Charts extends \NDB_Page private function _handleSiteEEGRecordingsBreakdown( ServerRequestInterface $request ) { - $DB = \NDB_Factory::singleton()->database(); - $user = \NDB_Factory::singleton()->user(); - $sites = $user->getSites(); + $DB = \NDB_Factory::singleton()->database(); + $user = \NDB_Factory::singleton()->user(); + $sites = $user->getSites(); $conditions = $this->_buildQueryConditions(); From ce3b6019a057334847e02696f73028d55b3b56b0 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 22 Oct 2025 09:29:27 -0400 Subject: [PATCH 33/40] Add request to endpoints --- modules/statistics/php/charts.class.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 3d6a397f346..58e331419ad 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -894,7 +894,7 @@ class Charts extends \NDB_Page $user = \NDB_Factory::singleton()->user(); $sites = $user->getSites(); - $conditions = $this->_buildQueryConditions(); + $conditions = $this->_buildQueryConditions($request); $data = iterator_to_array( $DB->pselect( @@ -955,7 +955,7 @@ class Charts extends \NDB_Page $user = \NDB_Factory::singleton()->user(); $projects = $user->getProjects(); - $conditions = $this->_buildQueryConditions(); + $conditions = $this->_buildQueryConditions($request); $data = iterator_to_array( $DB->pselect( From a3c698df2ce3eff7dc37b1668135b4cf84b5edba Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 29 Oct 2025 13:24:05 -0400 Subject: [PATCH 34/40] Delete redeclaration --- modules/statistics/php/charts.class.inc | 43 ------------------------- 1 file changed, 43 deletions(-) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 58e331419ad..04b1664ca65 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -1002,47 +1002,4 @@ class Charts extends \NDB_Page return (new \LORIS\Http\Response\JsonResponse($projectData)); } - - /** - * Handle an incoming request for EEG recordings by site breakdown. - * - * @return ResponseInterface - */ - private function _handleSiteEEGRecordingsBreakdown() - { - $DB = \NDB_Factory::singleton()->database(); - $user = \NDB_Factory::singleton()->user(); - $sites = $user->getStudySites(); - - $data = $DB->pselect( - "SELECT s.CenterID, - COUNT(distinct s.ID) as count - FROM physiological_file pf - LEFT JOIN session s ON (s.ID=pf.SessionID) - - GROUP BY s.CenterID - ORDER BY s.CenterID", - [] - ); - - $processed_data = []; - foreach ($data as $row) { - $processed_data[$row['CenterID']] = [ - 'CenterID' => $row['CenterID'], - 'Name' => $row['Name'], - 'count' => intval($row['count']), - ]; - } - $siteData = []; - foreach ($sites as $siteID => $siteName) { - if (in_array($siteID, array_keys($processed_data))) { - $siteData[] = [ - 'label' => $siteName, - 'total' => $processed_data[$siteID]['count'], - ]; - } - } - - return (new \LORIS\Http\Response\JsonResponse($siteData)); - } } From 254602f33a4880dc2254864e41bca525921687f4 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 29 Oct 2025 13:46:32 -0400 Subject: [PATCH 35/40] Add translations --- .../jsx/widgets/electrophysiology.js | 71 +++++++++++++------ .../jsx/widgets/helpers/chartBuilder.js | 1 + modules/statistics/php/charts.class.inc | 23 ++++++ 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/modules/statistics/jsx/widgets/electrophysiology.js b/modules/statistics/jsx/widgets/electrophysiology.js index 1b9fe27c2ec..08bf6e49fc2 100644 --- a/modules/statistics/jsx/widgets/electrophysiology.js +++ b/modules/statistics/jsx/widgets/electrophysiology.js @@ -1,9 +1,12 @@ import React, {useEffect, useState} from 'react'; import PropTypes from 'prop-types'; +import i18n from 'I18nSetup'; import Loader from 'Loader'; import Panel from 'Panel'; import {QueryChartForm} from './helpers/queryChartForm'; import {setupCharts} from './helpers/chartBuilder'; +import {useTranslation} from 'react-i18next'; +import jaStrings from '../../locale/ja/LC_MESSAGES/statistics.json'; /** * Electrophysiology - a widget containing statistics for EEG data. @@ -12,6 +15,7 @@ import {setupCharts} from './helpers/chartBuilder'; * @return {JSX.Element} */ const Electrophysiology = (props) => { + const {t} = useTranslation(); const [loading, setLoading] = useState(true); const [showFiltersBreakdown, setShowFiltersBreakdown] = useState(false); @@ -21,35 +25,35 @@ const Electrophysiology = (props) => { 'site_recordings': { 'eeg_recordings_by_site': { sizing: 11, - title: 'EEG Recordings by site', + title: t('EEG Recordings by site', {ns: 'statistics'}), filters: '', chartType: 'pie', dataType: 'pie', - label: 'EEG Recordings', + label: t('EEG Recordings', {ns: 'statistics'}), units: null, showPieLabelRatio: true, legend: '', options: {pie: 'pie', bar: 'bar'}, chartObject: null, - yLabel: 'EEG Recordings', - titlePrefix: 'Site', + yLabel: t('EEG Recordings', {ns: 'statistics'}), + titlePrefix: t('Site', {ns: 'loris'}), }, }, 'project_recordings': { 'eeg_recordings_by_project': { sizing: 11, - title: 'EEG Recordings by project', + title: t('EEG Recordings by project', {ns: 'statistics'}), filters: '', chartType: 'pie', dataType: 'pie', - label: 'EEG Recordings', + label: t('EEG Recordings', {ns: 'statistics'}), units: null, showPieLabelRatio: true, legend: '', options: {pie: 'pie', bar: 'bar'}, chartObject: null, - yLabel: 'EEG Recordings', - titlePrefix: 'Project', + yLabel: t('EEG Recordings', {ns: 'statistics'}), + titlePrefix: t('Project', {ns: 'loris'}), }, }, }); @@ -64,11 +68,15 @@ const Electrophysiology = (props) => { */ useEffect(() => { if (json && Object.keys(json).length !== 0) { - setupCharts(false, chartDetails).then((data) => { + setupCharts( + t, + false, + chartDetails, + t('Total', {ns: 'loris'}) + ).then((data) => { setChartDetails(data); }); json = props.data; - console.log('eeg_json', json); setLoading(false); } }, [props.data]); @@ -82,14 +90,18 @@ const Electrophysiology = (props) => { const getTotalRecordings = () => { return json['eeg_data']['total_recordings'] || -1; }; - + const title = (subtitle) => t('EEG data', {ns: 'statistics'}) + + ' — ' + t(subtitle, {ns: 'statistics'}); + const filterLabel = (hide) => hide ? + t('Hide Filters', {ns: 'loris'}) + : t('Show Filters', {ns: 'loris'}); return loading ? : ( <> { - setupCharts(false, chartDetails); + setupCharts(t, false, chartDetails, t('Total', {ns: 'loris'})); // reset filters when switching views setShowFiltersBreakdown(false); @@ -111,7 +123,7 @@ const Electrophysiology = (props) => { className="btn btn-default btn-xs" onClick={() => setShowFiltersBreakdown((prev) => !prev)} > - {showFiltersBreakdown ? 'Hide Filters' : 'Show Filters'} + {filterLabel(showFiltersBreakdown)}
{showFiltersBreakdown && ( @@ -128,10 +140,18 @@ const Electrophysiology = (props) => { {showChart('project_recordings', 'eeg_recordings_by_project')}
) : ( -

There is no data yet.

+

{t('There is no data yet.', {ns: 'statistics'})}

), - title: 'EEG data - recordings by project', - subtitle: 'Total recordings: ' + getTotalRecordings(), + + title: title('Recordings by Project'), + subtitle: t( + 'Total Recordings: {{count}}', + { + ns: 'statistics', + count: getTotalRecordings(), + } + ), + onToggleFilters: () => setShowFiltersBreakdown((prev) => !prev), }, { content: @@ -149,7 +169,7 @@ const Electrophysiology = (props) => { className="btn btn-default btn-xs" onClick={() => setShowFiltersBreakdown((prev) => !prev)} > - {showFiltersBreakdown ? 'Hide Filters' : 'Show Filters'} + {filterLabel(showFiltersBreakdown)}
{showFiltersBreakdown && ( @@ -166,11 +186,18 @@ const Electrophysiology = (props) => { {showChart('site_recordings', 'eeg_recordings_by_site')} ) : ( -

There is no data yet.

+

{t('There is no data yet.', {ns: 'statistics'})}

), - title: 'EEG data - recordings by site', - subtitle: 'Total recordings: ' + getTotalRecordings(), - }, + title: title('Recordings by Site'), + subtitle: t( + 'Total Recordings: {{count}}', + { + ns: 'statistics', + count: getTotalRecordings(), + } + ), + onToggleFilters: () => setShowFiltersBreakdown((prev) => !prev), + } ]} /> diff --git a/modules/statistics/jsx/widgets/helpers/chartBuilder.js b/modules/statistics/jsx/widgets/helpers/chartBuilder.js index 3b979a5576c..89a9e1f6c43 100644 --- a/modules/statistics/jsx/widgets/helpers/chartBuilder.js +++ b/modules/statistics/jsx/widgets/helpers/chartBuilder.js @@ -300,6 +300,7 @@ const setupCharts = async (t, targetIsModal, chartDetails, totalLabel) => { let labels = []; let colours = []; if (chart.dataType === 'pie') { + console.log('charDetails', chartDetails); columns = formatPieData(chartData); colours = siteColours; // reformating the columns for a bar chart when it was originally pie data diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 04b1664ca65..68b880d33eb 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -895,7 +895,26 @@ class Charts extends \NDB_Page $sites = $user->getSites(); $conditions = $this->_buildQueryConditions($request); + error_log(json_encode($conditions)); + + error_log("SELECT + s.CenterID, + psc.Name, + COUNT(distinct s.ID) as count + FROM physiological_file pf + JOIN session s ON (s.ID=pf.SessionID) + JOIN candidate c ON (c.ID=s.CandidateID) + JOIN psc USING (CenterID) + {$conditions['participantStatusJoin']} + WHERE 1 = 1 + {$conditions['projectQuery']} + {$conditions['cohortQuery']} + {$conditions['visitQuery']} + {$conditions['siteQuery']} + {$conditions['participantStatusQuery']} + GROUP BY s.CenterID + ORDER BY s.CenterID"); $data = iterator_to_array( $DB->pselect( "SELECT @@ -938,6 +957,10 @@ class Charts extends \NDB_Page } } + + error_log('sitedata'); + error_log(json_encode($siteData)); + return (new \LORIS\Http\Response\JsonResponse($siteData)); } From 113a60509f2a581551850a705346892850df776e Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 29 Oct 2025 13:49:55 -0400 Subject: [PATCH 36/40] Revert unecessary changes --- modules/statistics/php/charts.class.inc | 96 +++++++++---------------- 1 file changed, 35 insertions(+), 61 deletions(-) diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index 68b880d33eb..a1f58259ec5 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -100,14 +100,14 @@ class Charts extends \NDB_Page return $this->_handleScansByMonth($request); case 'siterecruitment_bymonth': return $this->_handleSiteLineData($request); - case 'size_byproject': - return $this->_handleProjectSizeBreakdown(); case 'agedistribution_line': return $this->_handleAgeDistributionByProject($request); case 'eeg_recordings_by_site': return $this->_handleSiteEEGRecordingsBreakdown($request); case 'eeg_recordings_by_project': return $this->_handleProjectEEGRecordingsBreakdown($request); + case 'size_byproject': + return $this->_handleProjectSizeBreakdown(); default: return new \LORIS\Http\Response\JSON\NotFound(); } @@ -352,7 +352,6 @@ class Charts extends \NDB_Page if (!isset($agesByProject[$projectID]['ages'])) { $agesByProject[$projectID]['ages'] = []; } - if (!isset($agesByProject[$projectID]['ages'][$age])) { $agesByProject[$projectID]['ages'][$age] = 1; } else { @@ -762,19 +761,17 @@ class Charts extends \NDB_Page } } - $startDate = $queryParams['dateParticipantRegisteredStart'] ?? 'undefined'; - $endDate = $queryParams['dateParticipantRegisteredEnd'] ?? 'undefined'; - if ($startDate != 'undefined') { + if (($queryParams['dateRegisteredStart'] ?? "undefined") != 'undefined') { $candJoin = "JOIN candidate c ON c.ID=s.CandidateID"; $paramName = 'dateStart' . (++$paramCounter); $projectQuery .= " AND c.Date_registered >= :$paramName"; - $params[$paramName] = $queryParams['dateParticipantRegisteredStart']; + $params[$paramName] = $queryParams['dateRegisteredStart']; } - if ($endDate != 'undefined') { + if (($queryParams['dateRegisteredEnd'] ?? "undefined") != 'undefined') { $candJoin = "JOIN candidate c ON c.ID=s.CandidateID"; $paramName = 'dateEnd' . (++$paramCounter); $projectQuery .= " AND c.Date_registered <= :$paramName"; - $params[$paramName] = $queryParams['dateParticipantRegisteredEnd']; + $params[$paramName] = $queryParams['dateRegisteredEnd']; } return [ @@ -895,46 +892,27 @@ class Charts extends \NDB_Page $sites = $user->getSites(); $conditions = $this->_buildQueryConditions($request); - error_log(json_encode($conditions)); - - error_log("SELECT - s.CenterID, - psc.Name, - COUNT(distinct s.ID) as count - FROM physiological_file pf - JOIN session s ON (s.ID=pf.SessionID) - JOIN candidate c ON (c.ID=s.CandidateID) - JOIN psc USING (CenterID) - {$conditions['participantStatusJoin']} - WHERE 1 = 1 - {$conditions['projectQuery']} - {$conditions['cohortQuery']} - {$conditions['visitQuery']} - {$conditions['siteQuery']} - {$conditions['participantStatusQuery']} - GROUP BY s.CenterID - ORDER BY s.CenterID"); $data = iterator_to_array( $DB->pselect( "SELECT s.CenterID, psc.Name, COUNT(distinct s.ID) as count - FROM physiological_file pf - JOIN session s ON (s.ID=pf.SessionID) - JOIN candidate c ON (c.ID=s.CandidateID) - JOIN psc USING (CenterID) - {$conditions['participantStatusJoin']} - WHERE 1 = 1 - {$conditions['projectQuery']} - {$conditions['cohortQuery']} - {$conditions['visitQuery']} - {$conditions['siteQuery']} - {$conditions['participantStatusQuery']} - GROUP BY s.CenterID - ORDER BY s.CenterID", - [] + FROM physiological_file pf + JOIN session s ON (s.ID=pf.SessionID) + JOIN candidate c ON (c.ID=s.CandidateID) + JOIN psc USING (CenterID) + {$conditions['participantStatusJoin']} + WHERE 1 = 1 + {$conditions['projectQuery']} + {$conditions['cohortQuery']} + {$conditions['visitQuery']} + {$conditions['siteQuery']} + {$conditions['participantStatusQuery']} + GROUP BY s.CenterID + ORDER BY s.CenterID", + $conditions['params'] ) ); @@ -957,10 +935,6 @@ class Charts extends \NDB_Page } } - - error_log('sitedata'); - error_log(json_encode($siteData)); - return (new \LORIS\Http\Response\JsonResponse($siteData)); } @@ -986,21 +960,21 @@ class Charts extends \NDB_Page s.ProjectID, p.Name, COUNT(distinct s.ID) as count - FROM physiological_file pf - JOIN session s ON (s.ID=pf.SessionID) - JOIN Project p USING (ProjectID) - JOIN candidate c ON (c.ID=s.CandidateID) - JOIN psc ON (s.CenterID = psc.CenterID) - {$conditions['participantStatusJoin']} - WHERE 1 = 1 - {$conditions['projectQuery']} - {$conditions['cohortQuery']} - {$conditions['visitQuery']} - {$conditions['siteQuery']} - {$conditions['participantStatusQuery']} - GROUP BY s.CenterID - ORDER BY s.CenterID", - [] + FROM physiological_file pf + JOIN session s ON (s.ID=pf.SessionID) + JOIN Project p USING (ProjectID) + JOIN candidate c ON (c.ID=s.CandidateID) + JOIN psc ON (s.CenterID = psc.CenterID) + {$conditions['participantStatusJoin']} + WHERE 1 = 1 + {$conditions['projectQuery']} + {$conditions['cohortQuery']} + {$conditions['visitQuery']} + {$conditions['siteQuery']} + {$conditions['participantStatusQuery']} + GROUP BY s.CenterID + ORDER BY s.CenterID", + $conditions['params'] ) ); From 2927838225d9c5578a818c1d52dfc8e397e353c5 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 29 Oct 2025 15:33:54 -0400 Subject: [PATCH 37/40] Linter compliance --- modules/statistics/jsx/WidgetIndex.js | 1 - .../jsx/widgets/electrophysiology.js | 93 ++++++++++++++++--- modules/statistics/php/charts.class.inc | 65 +++++++++++++ modules/statistics/php/widgets.class.inc | 15 ++- 4 files changed, 156 insertions(+), 18 deletions(-) diff --git a/modules/statistics/jsx/WidgetIndex.js b/modules/statistics/jsx/WidgetIndex.js index fdc9f7d33c4..a6fb9a60421 100644 --- a/modules/statistics/jsx/WidgetIndex.js +++ b/modules/statistics/jsx/WidgetIndex.js @@ -256,7 +256,6 @@ const WidgetIndex = (props) => { setRecruitmentData(data); setStudyProgressionData(data); setElectrophysiologyData(data); - }; setup().catch( (error) => { diff --git a/modules/statistics/jsx/widgets/electrophysiology.js b/modules/statistics/jsx/widgets/electrophysiology.js index 08bf6e49fc2..fea86cb1cdb 100644 --- a/modules/statistics/jsx/widgets/electrophysiology.js +++ b/modules/statistics/jsx/widgets/electrophysiology.js @@ -1,12 +1,10 @@ import React, {useEffect, useState} from 'react'; import PropTypes from 'prop-types'; -import i18n from 'I18nSetup'; import Loader from 'Loader'; import Panel from 'Panel'; import {QueryChartForm} from './helpers/queryChartForm'; import {setupCharts} from './helpers/chartBuilder'; import {useTranslation} from 'react-i18next'; -import jaStrings from '../../locale/ja/LC_MESSAGES/statistics.json'; /** * Electrophysiology - a widget containing statistics for EEG data. @@ -22,6 +20,23 @@ const Electrophysiology = (props) => { let json = props.data; const [chartDetails, setChartDetails] = useState({ + 'project_recordings': { + 'eeg_recordings_by_project': { + sizing: 11, + title: t('EEG Recordings by project', {ns: 'statistics'}), + filters: '', + chartType: 'pie', + dataType: 'pie', + label: t('EEG Recordings', {ns: 'statistics'}), + units: null, + showPieLabelRatio: true, + legend: '', + options: {pie: 'pie', bar: 'bar'}, + chartObject: null, + yLabel: t('EEG Recordings', {ns: 'statistics'}), + titlePrefix: t('Project', {ns: 'loris'}), + }, + }, 'site_recordings': { 'eeg_recordings_by_site': { sizing: 11, @@ -39,20 +54,20 @@ const Electrophysiology = (props) => { titlePrefix: t('Site', {ns: 'loris'}), }, }, - 'project_recordings': { - 'eeg_recordings_by_project': { + 'project_events': { + 'eeg_events_by_project': { sizing: 11, - title: t('EEG Recordings by project', {ns: 'statistics'}), + title: t('EEG Events by project', {ns: 'statistics'}), filters: '', chartType: 'pie', dataType: 'pie', - label: t('EEG Recordings', {ns: 'statistics'}), + label: t('EEG Events', {ns: 'statistics'}), units: null, showPieLabelRatio: true, legend: '', options: {pie: 'pie', bar: 'bar'}, chartObject: null, - yLabel: t('EEG Recordings', {ns: 'statistics'}), + yLabel: t('EEG Events', {ns: 'statistics'}), titlePrefix: t('Project', {ns: 'loris'}), }, }, @@ -90,6 +105,9 @@ const Electrophysiology = (props) => { const getTotalRecordings = () => { return json['eeg_data']['total_recordings'] || -1; }; + const getTotalEvents = () => { + return json['eeg_data']['total_events'] || -1; + }; const title = (subtitle) => t('EEG data', {ns: 'statistics'}) + ' — ' + t(subtitle, {ns: 'statistics'}); const filterLabel = (hide) => hide ? @@ -164,13 +182,13 @@ const Electrophysiology = (props) => { }} >
- +
{showFiltersBreakdown && ( { } ), onToggleFilters: () => setShowFiltersBreakdown((prev) => !prev), - } + }, + { + content: + getTotalEvents() > 0 ? ( +
+
+ +
+ {showFiltersBreakdown && ( + { + updateFilters(formDataObj, 'project_events'); + }} + /> + )} + {showChart('project_events', 'eeg_events_by_project')} +
+ ) : ( +

{t('There is no data yet.', {ns: 'statistics'})}

+ ), + title: title('Events by Project'), + subtitle: t( + 'Total Events: {{count}}', + { + ns: 'statistics', + count: getTotalEvents(), + } + ), + onToggleFilters: () => setShowFiltersBreakdown((prev) => !prev), + }, ]} /> diff --git a/modules/statistics/php/charts.class.inc b/modules/statistics/php/charts.class.inc index a1f58259ec5..3bdd6540c62 100644 --- a/modules/statistics/php/charts.class.inc +++ b/modules/statistics/php/charts.class.inc @@ -106,6 +106,8 @@ class Charts extends \NDB_Page return $this->_handleSiteEEGRecordingsBreakdown($request); case 'eeg_recordings_by_project': return $this->_handleProjectEEGRecordingsBreakdown($request); + case 'eeg_events_by_project': + return $this->_handleProjectEEGEventsBreakdown($request); case 'size_byproject': return $this->_handleProjectSizeBreakdown(); default: @@ -999,4 +1001,67 @@ class Charts extends \NDB_Page return (new \LORIS\Http\Response\JsonResponse($projectData)); } + + /** + * Handle an incoming request for EEG events by project breakdown. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface + */ + private function _handleProjectEEGEventsBreakdown( + ServerRequestInterface $request + ) { + $DB = \NDB_Factory::singleton()->database(); + $user = \NDB_Factory::singleton()->user(); + $projects = $user->getProjects(); + + $conditions = $this->_buildQueryConditions($request); + + $data = iterator_to_array( + $DB->pselect( + "SELECT + s.ProjectID, + p.Name, + COUNT(pte.PhysiologicalTaskEventID) as count + FROM physiological_task_event pte + JOIN physiological_file pf USING (PhysiologicalFileID) + JOIN session s ON (s.ID=pf.SessionID) + JOIN Project p USING (ProjectID) + JOIN candidate c ON (c.ID=s.CandidateID) + JOIN psc ON (s.CenterID = psc.CenterID) + {$conditions['participantStatusJoin']} + WHERE 1 = 1 + {$conditions['projectQuery']} + {$conditions['cohortQuery']} + {$conditions['visitQuery']} + {$conditions['siteQuery']} + {$conditions['participantStatusQuery']} + GROUP BY s.CenterID + ORDER BY s.CenterID", + $conditions['params'] + ) + ); + + $processedData = []; + foreach ($data as $row) { + $processedData[$row['ProjectID']] = [ + 'ProjectID' => $row['ProjectID'], + 'Name' => $row['Name'], + 'count' => intval($row['count']), + ]; + } + + $projectData = []; + foreach ($projects as $projectID => $projectName) { + if (in_array($projectID, array_keys($processedData))) { + $projectData[] = [ + 'label' => $projectName, + 'total' => $processedData[$projectID]['count'], + ]; + } + } + + return (new \LORIS\Http\Response\JsonResponse($projectData)); + } } diff --git a/modules/statistics/php/widgets.class.inc b/modules/statistics/php/widgets.class.inc index 37646f03066..672739a4b26 100644 --- a/modules/statistics/php/widgets.class.inc +++ b/modules/statistics/php/widgets.class.inc @@ -207,7 +207,7 @@ class Widgets extends \NDB_Page implements ETagCalculator } } - $eeg_data = $db->pselectOneInt( + $eeg_recordings = $db->pselectOneInt( "SELECT COUNT(distinct s.ID) as total_recordings FROM physiological_file pf JOIN session s ON (s.ID=pf.SessionID) @@ -216,6 +216,16 @@ class Widgets extends \NDB_Page implements ETagCalculator . ")", [] ); + $eeg_events = $db->pselectOneInt( + "SELECT COUNT(pte.PhysiologicalTaskEventID) as total_events + FROM physiological_task_event pte + JOIN physiological_file pf USING (PhysiologicalFileID) + JOIN session s ON (s.ID=pf.SessionID) + WHERE s.ProjectID IN (" . + join(',', $user->getProjectIDs()) + . ")", + [] + ); $participantStatusOptions = \Candidate::getParticipantStatusOptions(); @@ -243,7 +253,8 @@ class Widgets extends \NDB_Page implements ETagCalculator $values['size_byproject'] = $projectsSizes; $values['eeg_data'] = [ - 'total_recordings' => $eeg_data + 'total_recordings' => $eeg_recordings, + 'total_events' => $eeg_events ]; $this->_cache = new \LORIS\Http\Response\JsonResponse($values); From bbbb2263859abf2fa39e142151e79a9d3e682cf5 Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Wed, 29 Oct 2025 15:34:29 -0400 Subject: [PATCH 38/40] Linter compliance --- .../php/module.class.inc | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/modules/electrophysiology_browser/php/module.class.inc b/modules/electrophysiology_browser/php/module.class.inc index 2b25c283e3a..aeea265d9d5 100644 --- a/modules/electrophysiology_browser/php/module.class.inc +++ b/modules/electrophysiology_browser/php/module.class.inc @@ -82,8 +82,8 @@ class Module extends \Module switch ($type) { case 'study-progression': - $DB = $factory->database(); - $data = $DB->pselectWithIndexKey( + $DB = $factory->database(); + $sessionData = $DB->pselectWithIndexKey( "SELECT p.ProjectID, p.Name AS ProjectName, @@ -101,6 +101,25 @@ class Module extends \Module [], 'ProjectID' ); + $eventData = $DB->pselectWithIndexKey( + "SELECT + p.ProjectID, + p.Name AS ProjectName, + COUNT(pte.PhysiologicalTaskEventID) AS count, + CONCAT('" + . $baseURL . + "/electrophysiology_browser/?project=', p.Name + ) as url + FROM session s + JOIN Project p ON p.ProjectID = s.ProjectID + JOIN physiological_file pf ON pf.SessionID = s.ID + JOIN physiological_task_event pte USING (PhysiologicalFileID) + WHERE s.Active <> 'N' + AND s.CenterID <> 1 + GROUP BY p.Name", + [], + 'ProjectID' + ); return [ new \LORIS\dashboard\DataWidget( new \LORIS\GUI\LocalizableString( @@ -108,7 +127,17 @@ class Module extends \Module "EEG Session", "EEG Sessions", ), - $data, + $sessionData, + "", + 'rgb(186,255,201)', + ), + new \LORIS\dashboard\DataWidget( + new \LORIS\GUI\LocalizableString( + "dqt", + "EEG Event", + "EEG Events", + ), + $eventData, "", 'rgb(186,255,201)', ) From a59afe99937d54f373619b487d91938b29a1870c Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Thu, 30 Oct 2025 10:41:37 -0400 Subject: [PATCH 39/40] Static compliance --- modules/electrophysiology_browser/php/module.class.inc | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/electrophysiology_browser/php/module.class.inc b/modules/electrophysiology_browser/php/module.class.inc index aeea265d9d5..5ff397e8bdc 100644 --- a/modules/electrophysiology_browser/php/module.class.inc +++ b/modules/electrophysiology_browser/php/module.class.inc @@ -78,7 +78,6 @@ class Module extends \Module { $factory = \NDB_Factory::singleton(); $baseURL = $factory->settings()->getBaseURL(); - $projects = $user->getProjects(); switch ($type) { case 'study-progression': From 2acd59f9b5300b87110f08ec552ab8f224a2466e Mon Sep 17 00:00:00 2001 From: jeffersoncasimir Date: Thu, 30 Oct 2025 11:05:31 -0400 Subject: [PATCH 40/40] Static compliance --- modules/electrophysiology_browser/php/module.class.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/electrophysiology_browser/php/module.class.inc b/modules/electrophysiology_browser/php/module.class.inc index 5ff397e8bdc..f6e4fbe4425 100644 --- a/modules/electrophysiology_browser/php/module.class.inc +++ b/modules/electrophysiology_browser/php/module.class.inc @@ -76,8 +76,8 @@ class Module extends \Module */ public function getWidgets(string $type, \User $user, array $options) : array { - $factory = \NDB_Factory::singleton(); - $baseURL = $factory->settings()->getBaseURL(); + $factory = \NDB_Factory::singleton(); + $baseURL = $factory->settings()->getBaseURL(); switch ($type) { case 'study-progression':