From 58364fa40f67eab28b702ad4176ccbeb19cc4a8e Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:02:38 -0400 Subject: [PATCH 01/33] redcap dictionary table --- SQL/0000-00-00-schema.sql | 26 +++++++++++++++++++++++++- SQL/9999-99-99-drop_tables.sql | 1 + 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index ef0a7c39504..1f2cb671b0c 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -2656,4 +2656,28 @@ 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; + + +CREATE TABLE `redcap_dictionary` ( + ID int(10) unsigned NOT NULL auto_increment, + FormName varchar(255), + FieldName varchar(255) NOT NULL, + FieldRequired boolean NOT NULL, + FieldType varchar(20) NOT NULL, + FieldLabel text NOT NULL, + SectionHeader text, + Choices text, + FieldNote text, + TextValidationType varchar(50), + TextValidationMin varchar(50), + TextValidationMax varchar(50), + Identifier text, + BranchingLogic text, + CustomAlignment varchar(10), + QuestionNumber text, + MatrixGroupName varchar(100), + MatrixRanking varchar(50), + FieldAnnotation text, + PRIMARY KEY (`ID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='All REDCap instrument fields and variables'; \ No newline at end of file diff --git a/SQL/9999-99-99-drop_tables.sql b/SQL/9999-99-99-drop_tables.sql index c5579986fff..42a55138027 100644 --- a/SQL/9999-99-99-drop_tables.sql +++ b/SQL/9999-99-99-drop_tables.sql @@ -71,6 +71,7 @@ DROP TABLE IF EXISTS `modules`; -- 0000-00-00-schema.sql DROP TABLE IF EXISTS `redcap_notification`; +DROP TABLE IF EXISTS `redcap_dictionary`; DROP TABLE IF EXISTS `candidate_consent_rel`; DROP TABLE IF EXISTS `consent`; From 5bbba43096c16512b299d85a255271442b1d3a74 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:03:43 -0400 Subject: [PATCH 02/33] redcap ui view permission --- SQL/0000-00-02-Permission.sql | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SQL/0000-00-02-Permission.sql b/SQL/0000-00-02-Permission.sql index 3cbaf768f80..fcafa40f748 100644 --- a/SQL/0000-00-02-Permission.sql +++ b/SQL/0000-00-02-Permission.sql @@ -165,7 +165,8 @@ INSERT INTO `permissions` (code, description, moduleID, categoryID) VALUES ('imaging_uploader_nosessionid', 'Imaging Scans with no session ID', (SELECT ID FROM modules WHERE Name='imaging_uploader'),2), ('dicom_archive_nosessionid', 'DICOMs with no session ID', (SELECT ID FROM modules WHERE Name='dicom_archive'),2), ('dicom_archive_view_ownsites', 'DICOMs - Own Sites', (SELECT ID FROM modules WHERE Name='dicom_archive'),2), - ('view_instrument_data', 'Data', (SELECT ID FROM modules WHERE Name = 'instruments'),2) + ('view_instrument_data', 'Data', (SELECT ID FROM modules WHERE Name = 'instruments'),2), + ('redcap_ui_view','REDCap GUI - View',(SELECT ID FROM modules WHERE Name ='redcap'),2) ; INSERT INTO `user_perm_rel` (userID, permID) @@ -265,7 +266,9 @@ INSERT INTO `perm_perm_action_rel` (permID, actionID) VALUES ((SELECT permID FROM permissions WHERE code = 'imaging_uploader_nosessionid'),1), ((SELECT permID FROM permissions WHERE code = 'dicom_archive_nosessionid'),1), ((SELECT permID FROM permissions WHERE code = 'dicom_archive_view_ownsites'),1), - ((SELECT permID FROM permissions WHERE code = 'view_instrument_data'),1); + ((SELECT permID FROM permissions WHERE code = 'view_instrument_data'),1), + ((SELECT permID FROM permissions WHERE code = 'redcap_ui_view'),1) + ; -- permissions for each notification module From 5d1c3f87f54ea2348bde12a6c7356f418e25f510 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:04:05 -0400 Subject: [PATCH 03/33] patch --- ...2025-07-10_REDCap_front_end_permission.sql | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql diff --git a/SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql b/SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql new file mode 100644 index 00000000000..47f8a4cc9bd --- /dev/null +++ b/SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql @@ -0,0 +1,38 @@ +-- adds the redcap_ui_view permission +INSERT INTO permissions (code, description, moduleID, categoryID) + VALUES ( + 'redcap_ui_view', + 'REDCap GUI - View', + (SELECT ID FROM modules WHERE Name ='redcap'), + (SELECT ID FROM permissions_category WHERE Description = 'Permission') + ); + +INSERT INTO perm_perm_action_rel (permID, actionID) + VALUES ( + (SELECT permID FROM permissions WHERE code = 'redcap_ui_view'), + (SELECT ID FROM permisssions_action WHERE name = 'View') + ); + +-- adds the redcap dictionary table +CREATE TABLE `redcap_dictionary` ( + ID int(10) unsigned NOT NULL auto_increment, + FormName varchar(255), + FieldName varchar(255) NOT NULL, + FieldRequired boolean NOT NULL, + FieldType varchar(20) NOT NULL, + FieldLabel text NOT NULL, + SectionHeader text, + Choices text, + FieldNote text, + TextValidationType varchar(50), + TextValidationMin varchar(50), + TextValidationMax varchar(50), + Identifier text, + BranchingLogic text, + CustomAlignment varchar(10), + QuestionNumber text, + MatrixGroupName varchar(100), + MatrixRanking varchar(50), + FieldAnnotation text, + PRIMARY KEY (`ID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='All REDCap instrument fields and variables'; \ No newline at end of file From fb3ab83d315724b33159d365b6a30b610d3b90db Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:08:06 -0400 Subject: [PATCH 04/33] RB --- raisinbread/RB_files/RB_modules.sql | 1 + raisinbread/RB_files/RB_perm_perm_action_rel.sql | 1 + raisinbread/RB_files/RB_permissions.sql | 1 + 3 files changed, 3 insertions(+) diff --git a/raisinbread/RB_files/RB_modules.sql b/raisinbread/RB_files/RB_modules.sql index 8d3b64376b6..b256bb9cf98 100644 --- a/raisinbread/RB_files/RB_modules.sql +++ b/raisinbread/RB_files/RB_modules.sql @@ -50,5 +50,6 @@ INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (47,'electrophysiology_upl INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (48,'schedule_module','Y'); INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (49,'dataquery','Y'); INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (50,'oidc','N'); +INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (51,'redcap','N'); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_perm_perm_action_rel.sql b/raisinbread/RB_files/RB_perm_perm_action_rel.sql index 18c4be6d346..fc62ebad918 100644 --- a/raisinbread/RB_files/RB_perm_perm_action_rel.sql +++ b/raisinbread/RB_files/RB_perm_perm_action_rel.sql @@ -91,5 +91,6 @@ INSERT INTO `perm_perm_action_rel` (`permID`, `actionID`) VALUES (82,1); INSERT INTO `perm_perm_action_rel` (`permID`, `actionID`) VALUES (83,1); INSERT INTO `perm_perm_action_rel` (`permID`, `actionID`) VALUES (84,1); INSERT INTO `perm_perm_action_rel` (`permID`, `actionID`) VALUES (85,1); +INSERT INTO `perm_perm_action_rel` (`permID`, `actionID`) VALUES (86,1); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/raisinbread/RB_files/RB_permissions.sql b/raisinbread/RB_files/RB_permissions.sql index 4ae815a528d..fa4735ca7a1 100644 --- a/raisinbread/RB_files/RB_permissions.sql +++ b/raisinbread/RB_files/RB_permissions.sql @@ -77,5 +77,6 @@ INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categor INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (83,'dicom_archive_nosessionid','DICOMs with no session ID',15,2); INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (84,'dicom_archive_view_ownsites','DICOMs - Own Sites',15,2); INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (85,'view_instrument_data','Data',26,2); +INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (86,'redcap_ui_view','REDCap GUI - View',51,2); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; From fc421e02bf1bcaad19bfbdfd450cad9e4a573acb Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:16:18 -0400 Subject: [PATCH 05/33] backend endpoints for ui notification/issues/dictionary --- modules/redcap/php/dictionary.class.inc | 119 +++++++++++++++++ modules/redcap/php/issues.class.inc | 120 ++++++++++++++++++ modules/redcap/php/redcap.class.inc | 97 ++++++++++++++ .../redcap/php/redcapprovisioner.class.inc | 69 ++++++++++ modules/redcap/php/redcaprow.class.inc | 39 ++++++ 5 files changed, 444 insertions(+) create mode 100644 modules/redcap/php/dictionary.class.inc create mode 100644 modules/redcap/php/issues.class.inc create mode 100644 modules/redcap/php/redcap.class.inc create mode 100644 modules/redcap/php/redcapprovisioner.class.inc create mode 100644 modules/redcap/php/redcaprow.class.inc diff --git a/modules/redcap/php/dictionary.class.inc b/modules/redcap/php/dictionary.class.inc new file mode 100644 index 00000000000..1d63b4561c9 --- /dev/null +++ b/modules/redcap/php/dictionary.class.inc @@ -0,0 +1,119 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Dictionary extends \NDB_Page +{ + /** + * Check if user should be allowed to see this page. + * + * @param \User $user The user whose access is being checked + * + * @return bool true if the user is permitted to see violated scans + */ + function _hasAccess(\User $user) : bool + { + return $user->hasAnyPermission( + [ + 'redcap_ui_view', + ] + ); + } + + /** + * Return an array of valid HTTP methods for this endpoint + * + * @return string[] Valid versions + */ + protected function allowedMethods(): array + { + return [ + 'GET', + ]; + } + + /** + * This function will return a json response. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + return match ($request->getMethod()) { + 'GET' => $this->_handleGET($request), + default => new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ), + }; + } + + /** + * Handle an incoming HTTP GET request. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface + */ + private function _handleGET(ServerRequestInterface $request) : ResponseInterface + { + $db = $this->loris->getDatabaseConnection(); + $dictionary = $db->pselect( + "SELECT + FormName, + FieldName, + FieldRequired, + FieldType, + FieldLabel, + SectionHeader, + Choices, + FieldNote, + TextValidationType, + TextValidationMin, + TextValidationMax, + Identifier, + BranchingLogic, + CustomAlignment, + QuestionNumber, + MatrixGroupName, + MatrixRanking, + FieldAnnotation + FROM redcap_dictionary", + [] + ); + + if ($dictionary->count() === 0) { + return new \LORIS\Http\Response\JSON\OK([]); + } + + return new \LORIS\Http\Response\JSON\OK( + array_map( + fn($v) => array_values($v), + iterator_to_array($dictionary->getIterator()) + ) + ); + } +} \ No newline at end of file diff --git a/modules/redcap/php/issues.class.inc b/modules/redcap/php/issues.class.inc new file mode 100644 index 00000000000..1d149360da0 --- /dev/null +++ b/modules/redcap/php/issues.class.inc @@ -0,0 +1,120 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Issues extends \NDB_Page +{ + /** + * Check if user should be allowed to see this page. + * + * @param \User $user The user whose access is being checked + * + * @return bool true if the user is permitted to see violated scans + */ + function _hasAccess(\User $user) : bool + { + return $user->hasAnyPermission( + [ + 'redcap_ui_view', + ] + ); + } + + /** + * Return an array of valid HTTP methods for this endpoint + * + * @return string[] Valid versions + */ + protected function allowedMethods(): array + { + return [ + 'GET', + ]; + } + + /** + * This function will return a json response. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface The outgoing PSR7 response + */ + public function handle(ServerRequestInterface $request) : ResponseInterface + { + // Ensure GET or POST request. + switch ($request->getMethod()) { + case 'GET': + return $this->_handleGET($request); + default: + return new \LORIS\Http\Response\JSON\MethodNotAllowed( + $this->allowedMethods() + ); + } + } + + /** + * Handle an incoming HTTP GET request. + * + * @param ServerRequestInterface $request The incoming PSR7 request + * + * @return ResponseInterface + */ + private function _handleGET(ServerRequestInterface $request) : ResponseInterface + { + $db = $this->loris->getDatabaseConnection(); + $issues = $db->pselect( + "SELECT + i.issueID, + i.title, + i.status, + i.dateCreated, + ( + SELECT ic.issueComment + FROM issues_comments ic + WHERE ic.issueID = i.issueID + ORDER BY ic.dateAdded DESC + LIMIT 1 + ) as description + FROM issues i + JOIN modules m ON (m.ID = i.module) + WHERE m.Name = 'redcap' + ORDER BY i.dateCreated DESC + LIMIT 10000 + ", + [] + ); + + if ($issues->count() === 0) { + return new \LORIS\Http\Response\JSON\OK([]); + } + + return new \LORIS\Http\Response\JSON\OK( + array_map( + fn($v) => array_values($v), + iterator_to_array($issues->getIterator()) + ) + ); + } +} \ No newline at end of file diff --git a/modules/redcap/php/redcap.class.inc b/modules/redcap/php/redcap.class.inc new file mode 100644 index 00000000000..c4dda3bd283 --- /dev/null +++ b/modules/redcap/php/redcap.class.inc @@ -0,0 +1,97 @@ + + * @license Loris license + * @link https://www.github.com/aces/Loris + */ +namespace LORIS\redcap; + +/** + * Implement the menu filter for searching through redcap data + * + * @category REDCap + * @package Main + * @author Regis Ongaro-Carcy + * @license Loris license + * @link https://www.github.com/aces/Loris + */ +class REDCap extends \DataFrameworkMenu +{ + /** + * Check if user should be allowed to see this page. + * + * @param \User $user The user whose access is being checked + * + * @return bool true if the user is permitted to see violated scans + */ + public function _hasAccess(\User $user) : bool + { + return $user->hasAnyPermission( + [ + 'redcap_ui_view', + ] + ); + } + + /** + * Tells the base class that this page's provisioner can support the + * HasAnyPermissionOrUserSiteMatch filter. + * + * @return ?array of site permissions or null + */ + public function allSitePermissionNames() : ?array + { + return null; + } + + /** + * Set up the variables required by NDB_Menu_Filter class for + * constructing a query + * + * @return array list of sites + */ + public function getFieldOptions(): array + { + return []; + } + + /** + * Tells the base class that this page's provisioner can support + * the UserProjectMatch filter. + * + * @return bool always true + */ + public function useProjectFilter(): bool + { + return false; + } + + /** + * Gets the data source for this menu filter. + * + * @return \LORIS\Data\Provisioner + */ + public function getBaseDataProvisioner() : \LORIS\Data\Provisioner + { + return new REDCapProvisioner($this->loris); + } + + /** + * Overrides base getJSDependencies() to add support for a React file. + * + * @return array of extra JS files that this page depends on + */ + public function getJSDependencies(): array + { + $factory = \NDB_Factory::singleton(); + $baseurl = $factory->settings()->getBaseURL(); + return array_merge( + parent::getJSDependencies(), + [$baseurl . "/redcap/js/redcapIndex.js"] + ); + } +} \ No newline at end of file diff --git a/modules/redcap/php/redcapprovisioner.class.inc b/modules/redcap/php/redcapprovisioner.class.inc new file mode 100644 index 00000000000..4e503006648 --- /dev/null +++ b/modules/redcap/php/redcapprovisioner.class.inc @@ -0,0 +1,69 @@ +DBRow = $row; + } + + /** + * Implements \LORIS\Data\DataInstance interface for this row. + * + * @return array which can be serialized by json_encode() + */ + public function jsonSerialize() : array + { + return $this->DBRow; + } +} \ No newline at end of file From 763b6f0aa08b2b3f87b5898a40116a5709c97912 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:17:35 -0400 Subject: [PATCH 06/33] queries update for redcap dictionary mgmt --- modules/redcap/php/queries.class.inc | 112 +++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/modules/redcap/php/queries.class.inc b/modules/redcap/php/queries.class.inc index 29c4008a56f..911a6d05bd1 100644 --- a/modules/redcap/php/queries.class.inc +++ b/modules/redcap/php/queries.class.inc @@ -16,6 +16,7 @@ namespace LORIS\redcap; use \LORIS\LorisInstance; +use LORIS\redcap\client\models\RedcapDictionaryRecord; use \LORIS\StudyEntities\Candidate\CandID; /** @@ -283,4 +284,115 @@ class Queries { $this->_db->run("UNLOCK TABLES"); } + + /** + * Tells if the instrument name is registered in the REDCap dictionary. + * + * @param string $instrument_name the instrument name to search + * + * @return bool true if the instrument name exists, else false. + */ + public function instrumentExistsInREDCapDictionary(string $instrument_name): bool + { + $dictionary_entry = $this->_db->pselectOne( + "SELECT DISTINCT FormName + FROM redcap_dictionary + WHERE FormName = :fn + ", + ["fn" => $instrument_name] + ); + return $dictionary_entry !== null; + } + + /** + * Get a specific REDCap Dictionary entry. + * + * @param string $form_name the searched form name. + * @param string $field_name the searched field name. + * + * @return client\models\RedcapDictionaryRecord|null + */ + public function getDictionaryEntry( + string $form_name, + string $field_name + ): ?client\models\RedcapDictionaryRecord { + $dictionary_entry = $this->_db->pselectRow( + "SELECT + FieldName, + FormName, + SectionHeader, + FieldType, + FieldLabel, + Choices, + FieldNote, + TextValidationType, + TextValidationMin, + TextValidationMax, + Identifier, + BranchingLogic, + FieldRequired, + CustomAlignment, + QuestionNumber, + MatrixGroupName, + MatrixRanking, + FieldAnnotation + FROM redcap_dictionary + WHERE FormName = :form_name + AND FieldName = :field_name + ", + [ + "form_name" => $form_name, + "field_name" => $field_name + ] + ); + + if ($dictionary_entry === null) { + return null; + } + + // dictionary props + $clean_props = array_combine( + RedcapDictionaryRecord::getHeaders(), + array_values($dictionary_entry) + ); + + // build the object based on props + return new RedcapDictionaryRecord($clean_props, false); + } + + /** + * Insert a REDCap dictionary entry. + * + * @param RedcapDictionaryRecord $dictionary_entry the dicitonary entry to insert + * + * @return void + */ + public function insertREDCapDictionaryEntry( + RedcapDictionaryRecord $dictionary_entry + ): void { + $this->_db->insert( + 'redcap_dictionary', + $dictionary_entry->toDatabaseArray() + ); + } + + /** + * Update a REDCap dictionary entry. + * + * @param RedcapDictionaryRecord $dictionary_entry the dicitonary entry to insert + * + * @return void + */ + public function updateREDCapDictionaryEntry( + RedcapDictionaryRecord $dictionary_entry + ): void { + $this->_db->update( + 'redcap_dictionary', + $dictionary_entry->toDatabaseArray(), + [ + "FormName" => $dictionary_entry->form_name, + "FieldName" => $dictionary_entry->field_name + ] + ); + } } From e74e7287e1a90069bb2a6ba102eea6d904ee7d53 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:18:22 -0400 Subject: [PATCH 07/33] dictionary record db export method --- .../models/redcapdictionaryrecord.class.inc | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/modules/redcap/php/client/models/redcapdictionaryrecord.class.inc b/modules/redcap/php/client/models/redcapdictionaryrecord.class.inc index 2f7bfb2bdfb..a9b4412c540 100644 --- a/modules/redcap/php/client/models/redcapdictionaryrecord.class.inc +++ b/modules/redcap/php/client/models/redcapdictionaryrecord.class.inc @@ -230,6 +230,54 @@ class RedcapDictionaryRecord ]; } + /** + * Used to insert the diciotnary entry in the db redcap_dictionary. + * + * @return array{ + * branching_logic: string|null, + * custom_alignment: string|null, + * field_annotation: string|null, + * field_label: string, + * field_name: string, + * field_note: string|null, + * field_type: string, + * form_name: string, + * identifier: string|null, + * matrix_group_name: string|null, + * matrix_ranking: string|null, + * question_number: string|null, + * required_field: bool, + * section_header: string|null, + * select_choices_or_calculations: string|null, + * text_validation_max: string|null, + * text_validation_min: string|null, + * text_validation_type_or_show_slider_number: string|null + * } + */ + public function toDatabaseArray(): array + { + return [ + 'FieldName' => $this->field_name, + 'FormName' => $this->form_name, + 'SectionHeader' => $this->section_header, + 'FieldType' => $this->field_type, + 'FieldLabel' => $this->field_label, + 'Choices' => $this->select_choices, + 'FieldNote' => $this->field_note, + 'TextValidationType' => $this->text_validation_type, + 'TextValidationMin' => $this->text_validation_min, + 'TextValidationMax' => $this->text_validation_max, + 'Identifier' => $this->identifier, + 'BranchingLogic' => $this->branching_logic, + 'FieldRequired' => $this->required_field, + 'CustomAlignment' => $this->custom_alignment, + 'QuestionNumber' => $this->question_number, + 'MatrixGroupName' => $this->matrix_group_name, + 'MatrixRanking' => $this->matrix_ranking, + 'FieldAnnotation' => $this->field_annotation, + ]; + } + /** * Get the LINST version of that dicitonary record * From b699b8a84125ad2475aa607ee1ce37b223cd7f15 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:18:55 -0400 Subject: [PATCH 08/33] redcap menu category/menu link --- modules/redcap/php/module.class.inc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/redcap/php/module.class.inc b/modules/redcap/php/module.class.inc index 0f325c1f856..5dd0d9feaec 100644 --- a/modules/redcap/php/module.class.inc +++ b/modules/redcap/php/module.class.inc @@ -52,6 +52,16 @@ class Module extends \Module return "REDCap"; } + /** + * {@inheritDoc} + * + * @return ?\LORIS\GUI\MenuCategory The menu category for this module + */ + public function getMenuCategory() : ?\LORIS\GUI\MenuCategory + { + return \LORIS\GUI\MenuCategory::singleton("Tools"); + } + /** * Perform routing for this module's endpoints. * From e6d863e3ca77dbdbd46723e27313b41a7b3c9689 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:20:10 -0400 Subject: [PATCH 09/33] redcap2linst to redcap/tools and automatic update to dictonary when parsing redcap dd --- .../redcap/tools}/redcap2linst.php | 185 +++++++++++++----- 1 file changed, 132 insertions(+), 53 deletions(-) rename {tools => modules/redcap/tools}/redcap2linst.php (68%) diff --git a/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php similarity index 68% rename from tools/redcap2linst.php rename to modules/redcap/tools/redcap2linst.php index 83d0ad02296..724f39ea003 100644 --- a/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -16,18 +16,24 @@ * @link https://www.github.com/aces/Loris/ */ -require_once __DIR__ . "/generic_includes.php"; + require_once __DIR__ . "/../../../tools/generic_includes.php"; -// ini_set('display_errors', 1); -// ini_set('display_startup_errors', 1); -// error_reporting(E_ALL); + // load redcap module + try { + $lorisInstance->getModule('redcap')->registerAutoloader(); + } catch (\LorisNoSuchModuleException $th) { + error_log("[error] no 'redcap' module found."); + exit(1); + } -// load redcap module to use the client -$lorisInstance->getModule('redcap')->registerAutoloader(); +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); use LORIS\redcap\client\RedcapHttpClient; use LORIS\redcap\client\models\RedcapDictionaryRecord; use LORIS\redcap\config\RedcapConfigParser; +use LORIS\redcap\Queries; // options $opts = getopt( @@ -70,10 +76,12 @@ // create LINST lines by instrument fwrite(STDOUT, "\n-- Parsing records...\n"); $instruments = []; +$dictionary = []; foreach ($dict as $dict_record) { $linst = $dict_record->toLINST(); if (!empty($linst)) { $instruments[$dict_record->form_name][] = $linst; + $dictionary[$dict_record->form_name][] = $dict_record; } } @@ -83,9 +91,8 @@ // back-end name/title instrument mapping $redcap_intruments_map = ($options['redcapConnection'])->getInstruments(true); -fwrite(STDOUT, "\n-- Writing LINST/META files.\n\n"); - // write instrument +fwrite(STDOUT, "\n-- Writing LINST/META files.\n\n"); foreach ($instruments as $instrument_name => $instrument) { writeLINSTFile( $options['outputDir'], @@ -95,11 +102,110 @@ ); } +// update the db redcap_dictionary table per instrument +fwrite(STDOUT, "\n-- Writing entries to database 'redcap_dictionary' table.\n\n"); +$queries = new Queries($lorisInstance); +$nbEntriesCount = [ + 'created' => 0, + 'updated' => 0, + 'untouched' => 0 +]; +foreach ($dictionary as $instrument_name => $dictionary_entry) { + $nbEntries = updateREDCapDictionaryInstrumentEntries( + $queries, + $instrument_name, + $dictionary_entry + ); + + // update count + $nbEntriesCount['created'] += $nbEntries['created']; + $nbEntriesCount['updated'] += $nbEntries['updated']; + $nbEntriesCount['untouched'] += $nbEntries['untouched']; +} + +// db log +fwrite(STDOUT, "\n -> 'redcap_dictionary' table changes:"); +fwrite(STDOUT, "\n - fields created: {$nbEntriesCount['created']}"); +fwrite(STDOUT, "\n - fields updated: {$nbEntriesCount['updated']}"); +fwrite(STDOUT, "\n - fields untouched: {$nbEntriesCount['untouched']}\n"); + fwrite(STDOUT, "\n-- end\n"); // ------------ Functions +/** + * Updates the redcap_diciotnary table in db. + * + * @param LORIS\redcap\Queries $queries queries object + * @param string $instrument_name the instrument name to update + * @param RedcapDictionaryRecord[] $redcap_dictionary the redcap dictionary + * + * @return int[] an array of #created and #updated entries for this instrument + */ +function updateREDCapDictionaryInstrumentEntries( + Queries $queries, + string $instrument_name, + array $redcap_dictionary +): array { + // updating or creating new entry? + $instrumentExists = $queries->instrumentExistsInREDCapDictionary( + $instrument_name + ); + $updateMsg = $instrumentExists ? "updating" : "creating"; + fwrite(STDOUT, " -> {$updateMsg} '{$instrument_name}'\n"); + + // count + $nbEntriesCount = [ + 'created' => 0, + 'updated' => 0, + 'untouched' => 0 + ]; + + // go through entries for this instrument + foreach ($redcap_dictionary as $redcap_dictionary_entry) { + // get the db correspoding field name + $db_dictionary_field = $queries->getDictionaryEntry( + $redcap_dictionary_entry->form_name, + $redcap_dictionary_entry->field_name + ); + + // new entry => insert + if ($db_dictionary_field === null) { + $queries->insertREDCapDictionaryEntry($redcap_dictionary_entry); + $nbEntriesCount['created'] += 1; + continue; + } + + // escape HTML from this entry to compare + // same method use in Database class -> update + $escaped_props = array_map(fn($v) => is_string($v) ? htmlspecialchars( + $v, + ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, + 'UTF-8', + false + ) : $v, $redcap_dictionary_entry->toArray()); + $redcap_escaped_entry = new RedcapDictionaryRecord( + array_combine( + RedcapDictionaryRecord::getHeaders(), + array_values($escaped_props) + ) + ); + + // same entry, nothing to do + if ($db_dictionary_field == $redcap_escaped_entry) { + $nbEntriesCount['untouched'] += 1; + continue; + } + + // different entry => update + $queries->updateREDCapDictionaryEntry($redcap_dictionary_entry); + $nbEntriesCount['updated'] += 1; + } + + return $nbEntriesCount; +} + /** * Get the dictionary from a CSV file. * @@ -152,19 +258,22 @@ function getDictionaryCSVStream( // metadata $metadata = $row; + // combine headers/data + $payload = array_combine($headers, $metadata); + // do not trim if (!$trim_name) { - $dd = new RedcapDictionaryRecord(zip($headers, $metadata)); + $dd = new RedcapDictionaryRecord($payload); $mapped++; } else { // try to trim form name try { - $dd = new RedcapDictionaryRecord(zip($headers, $metadata), true); + $dd = new RedcapDictionaryRecord($payload, true); $mapped++; } catch (\LorisException $le) { // print error but continue with non trimmed fprintf(STDERR, $le->getMessage()); - $dd = new RedcapDictionaryRecord(zip($headers, $metadata)); + $dd = new RedcapDictionaryRecord($payload); $badMap++; } } @@ -182,35 +291,6 @@ function getDictionaryCSVStream( return $dictionary; } -/** - * Zips values from the first array as keys with the values from the second - * array as values. Such as: - * ``` - * # input - * $a1 = [0 => 'a', 1 => 'b', 2 => 'c'] - * $a2 = [0 => 'yes', 1 => 'no', 2 => 'maybe'] - * # result - * $zipped = ['a' => 'yes', 'b' => 'no', 'c' => 'maybe'] - * ``` - * Both arrays must have the same length. - * - * @param array $headers the key array - * @param array $metadata the value array. - * - * @return array a zipped array. - */ -function zip(array &$headers, array &$metadata): array -{ - if (count($headers) !== count($metadata)) { - fwrite(STDERR, "Cannot zip headers with metadata.\n"); - } - $zipped = []; - foreach ($headers as $i => $h) { - $zipped[$h] = $metadata[$i]; - } - return $zipped; -} - /** * Write LINST file and its associated META file. * @@ -227,12 +307,12 @@ function writeLINSTFile( string $instrument_title, array $instrument ): void { - fwrite(STDERR, " -> writing '$instrument_name'\n"); + fwrite(STDOUT, " -> writing '{$instrument_name}'\n"); // - $fp = fopen("$output_dir/$instrument_name.linst", "w"); - fwrite($fp, "{-@-}testname{@}$instrument_name\n"); - fwrite($fp, "table{@}$instrument_name\n"); - fwrite($fp, "title{@}$instrument_title\n"); + $fp = fopen("{$output_dir}/{$instrument_name}.linst", "w"); + fwrite($fp, "{-@-}testname{@}{$instrument_name}\n"); + fwrite($fp, "table{@}{$instrument_name}\n"); + fwrite($fp, "title{@}{$instrument_title}\n"); // Standard LORIS metadata fields that the instrument builder adds // and LINST class automatically adds to instruments. @@ -255,15 +335,14 @@ function writeLINSTFile( } else { // write field line fwrite($fp, "$field\n"); - } } fclose($fp); // META file - $fp_meta = fopen("$output_dir/$instrument_name.meta", "w"); - fwrite($fp_meta, "testname{@}$instrument_name\n"); - fwrite($fp_meta, "table{@}$instrument_name\n"); + $fp_meta = fopen("{$output_dir}/{$instrument_name}.meta", "w"); + fwrite($fp_meta, "testname{@}{$instrument_name}\n"); + fwrite($fp_meta, "table{@}{$instrument_name}\n"); fwrite($fp_meta, "jsondata{@}true\n"); fwrite($fp_meta, "norules{@}true"); fclose($fp_meta); @@ -279,7 +358,7 @@ function showHelp() global $argv; fprintf( STDERR, - "\nUsage: $argv[0]\n\n" + "\nUsage: {$argv[0]}\n\n" . "Creates LINST files based on an input file" . " or directly from a REDCap connection.\n" . "A valid REDCap connection is required to get the instruments back-end" @@ -319,11 +398,11 @@ function checkOptions(\LORIS\LorisInstance $loris, array &$options): array exit(1); } if (!is_dir($output_dir)) { - fprintf(STDERR, "Output directory '$output_dir' does not exist.\n"); + fprintf(STDERR, "Output directory '{$output_dir}' does not exist.\n"); exit(1); } if (!is_writeable($output_dir)) { - fprintf(STDERR, "Output directory '$output_dir' is not writeable.\n"); + fprintf(STDERR, "Output directory '{$output_dir}' is not writeable.\n"); exit(1); } @@ -351,7 +430,7 @@ function checkOptions(\LORIS\LorisInstance $loris, array &$options): array if (!is_file($input_file)) { fprintf( STDERR, - "Input file '$input_file' does not exist or is not readable.\n" + "Input file '{$input_file}' does not exist or is not readable.\n" ); exit(1); } From 559af395c5412e94a9b09c2ef343764d4a8fb45a Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:20:44 -0400 Subject: [PATCH 10/33] redcap ui 3 tabs --- modules/redcap/jsx/redcapIndex.js | 67 +++++ modules/redcap/jsx/tabs/dictionaryViewer.js | 255 ++++++++++++++++++ modules/redcap/jsx/tabs/issuesViewer.js | 208 ++++++++++++++ modules/redcap/jsx/tabs/notificationViewer.js | 196 ++++++++++++++ 4 files changed, 726 insertions(+) create mode 100644 modules/redcap/jsx/redcapIndex.js create mode 100644 modules/redcap/jsx/tabs/dictionaryViewer.js create mode 100644 modules/redcap/jsx/tabs/issuesViewer.js create mode 100644 modules/redcap/jsx/tabs/notificationViewer.js diff --git a/modules/redcap/jsx/redcapIndex.js b/modules/redcap/jsx/redcapIndex.js new file mode 100644 index 00000000000..f6f1181c6ba --- /dev/null +++ b/modules/redcap/jsx/redcapIndex.js @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types'; +import {TabPane, Tabs} from 'jsx/Tabs'; +import NotificationViewer from './tabs/notificationViewer'; +import DictionaryViewer from './tabs/dictionaryViewer'; +import IssuesViewer from './tabs/issuesViewer'; +import {createRoot} from 'react-dom/client'; + + +/** + * REDCap index view + * + * @param {array} props + * @return {JSX} + */ +export default function RedcapIndex(props) { + + // list of tabs + const tabList = [ + {id: 'notificationTable', label: 'Notifications'}, + {id: 'dictionaryTable', label: 'Data Dictionary'}, + {id: 'issuesTable', label: 'Issues'}, + ]; + + return ( + <> + + + {/* notifications */} + + + + + {/* dictionary */} + + + + + {/* issues */} + + + + + + + ); +} + +RedcapIndex.propTypes = { + baseURL: PropTypes.string.isRequired, + moduleURL: PropTypes.string.isRequired, + hasPermission: PropTypes.func.isRequired, +}; + +/** + * Render REDCap main page on page load + */ +document.addEventListener('DOMContentLoaded', function() { + createRoot( + document.getElementById('lorisworkspace') + ).render( + + ); +}); \ No newline at end of file diff --git a/modules/redcap/jsx/tabs/dictionaryViewer.js b/modules/redcap/jsx/tabs/dictionaryViewer.js new file mode 100644 index 00000000000..8db6eb843a8 --- /dev/null +++ b/modules/redcap/jsx/tabs/dictionaryViewer.js @@ -0,0 +1,255 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import Loader from 'jsx/Loader'; +import FilterableDataTable from 'jsx/FilterableDataTable'; + +/** + * Dictionary viewer component + */ +class DictionaryViewer extends Component { + + /** + * Constructor of component + * + * @param {object} props - the component properties. + */ + constructor(props) { + super(props); + + this.state = { + baseURL: props.baseURL, + data: {}, + error: false, + isLoaded: false, + }; + + this.fetchData = this.fetchData.bind(this); + this.formatColumn = this.formatColumn.bind(this); + } + + /** + * Called by React when the component has been rendered on the page. + */ + componentDidMount() { + this.fetchData(); + } + + /** + * Retrieve data for the form and save it in state. + */ + fetchData() { + fetch(`${this.state.baseURL}/dictionary`, + { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + } + ).then((resp) => { + if (!resp.ok) { + this.setState({error: true}); + console.error(resp.status + ': ' + resp.statusText); + return; + } + + resp.json() + .then((data) => { + this.setState({ + data, + isLoaded: true, + }); + }); + + }).catch((error) => { + this.setState({error: true}); + console.error(error); + }); + } + + /** + * Modify behaviour of specified column cells in the Data Table component + * + * @param {string} column - column name + * @param {string} cell - cell content + * @param {array} rowData - array of cell contents for a specific row + * @param {array} rowHeaders - array of table headers (column names) + * @return {*} a formatted table cell for a given column + */ + formatColumn(column, cell, rowData, rowHeaders) { + let result = ({cell}); + + // background color + let bgGreen = {textAlign: "center", backgroundColor: "#EDF7E5"}; + let bgRed = {textAlign: "center", backgroundColor: "#F7D6E0"}; + let bgYellow = {textAlign: "center", backgroundColor: "#FFF696"}; + + switch (column) { + case 'Required': + // yes/no answers + result = (cell === '1') + ? (✅) + : (🟨); + break; + default: + result = ({cell}); + break; + } + return result; + } + + /** + * @return {JSX} the incomplete form to render. + */ + render() { + // Waiting for async data to load. + if (!this.state.isLoaded) { + return ; + } + + // The fields configured for display/hide. + let fields = [ + { + label: "Instrument", + show: true, + type: 'text', + filter: { + name: 'Instrument', + type: 'text', + } + }, + { + label: "Field", + show: true, + type: 'text', + filter: { + name: 'Field', + type: 'text', + } + }, + { + label: "Required", + show: true, + type: 'text', + }, + { + label: "Type", + show: true, + type: 'text', + filter: { + name: 'Type', + type: 'text', + } + }, + { + label: "Label", + show: true, + type: 'text', + filter: { + name: 'Label', + type: 'text', + } + }, + { + label: "Section Header", + show: false, + type: 'text', + filter: { + name: 'Section Header', + type: 'text', + } + }, + { + label: "Choices", + show: true, + type: 'text', + filter: { + name: 'Choices', + type: 'text', + } + }, + { + label: "Note", + show: true, + type: 'text', + filter: { + name: 'Project ID', + type: 'text', + } + }, + { + label: "Validation Type", + show: true, + type: 'text', + }, + { + label: "Validation Min", + show: true, + type: 'text', + }, + { + label: "Validation Max", + show: true, + type: 'text', + }, + { + label: "Identifier", + show: true, + type: 'text', + }, + { + label: "Branching Logic", + show: true, + type: 'text', + }, + { + label: "Custom Alignment", + show: true, + type: 'text', + }, + { + label: "Question Number", + show: true, + type: 'text', + }, + { + label: "Matrix Group", + show: true, + type: 'text', + }, + { + label: "Matrix Ranking", + show: true, + type: 'text', + }, + { + label: "Annotation", + show: true, + type: 'text', + }, + ]; + + return ( +
+ {this.state.data == null + ? (
+ Error: no Data Dictionary entry found. +
) + : + } +
+ ); + } +} + +DictionaryViewer.propTypes = { + data: PropTypes.object, + baseURL: PropTypes.string.isRequired, +}; + +export default DictionaryViewer; \ No newline at end of file diff --git a/modules/redcap/jsx/tabs/issuesViewer.js b/modules/redcap/jsx/tabs/issuesViewer.js new file mode 100644 index 00000000000..a85fec4a8a2 --- /dev/null +++ b/modules/redcap/jsx/tabs/issuesViewer.js @@ -0,0 +1,208 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import Loader from 'jsx/Loader'; +import FilterableDataTable from 'jsx/FilterableDataTable'; + +/** + * Issues viewer component + */ +class IssuesViewer extends Component { + + /** + * Constructor of component + * + * @param {object} props - the component properties. + */ + constructor(props) { + super(props); + + this.state = { + baseURL: props.baseURL, + moduleURL: props.moduleURL, + data: {}, + error: false, + isLoaded: false, + }; + + this.fetchData = this.fetchData.bind(this); + this.formatColumn = this.formatColumn.bind(this); + } + + /** + * Called by React when the component has been rendered on the page. + */ + componentDidMount() { + this.fetchData(); + } + + /** + * Retrieve data and save it in state. + */ + fetchData() { + fetch(`${this.state.moduleURL}/issues`, + { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + } + ).then((resp) => { + if (!resp.ok) { + this.setState({error: true}); + console.error(resp.status + ': ' + resp.statusText); + return; + } + + resp.json() + .then((data) => { + this.setState({ + data, + isLoaded: true, + }); + }); + + }).catch((error) => { + this.setState({error: true}); + console.error(error); + }); + } + + /** + * Modify behaviour of specified column cells in the Data Table component + * + * @param {string} column - column name + * @param {string} cell - cell content + * @param {array} rowData - array of cell contents for a specific row + * @param {array} rowHeaders - array of table headers (column names) + * @return {*} a formatted table cell for a given column + */ + formatColumn(column, cell, rowData, rowHeaders) { + let result = ({cell}); + + // background color + let bgGreen = {textAlign: "center", backgroundColor: "#EDF7E5"}; + let bgRed = {textAlign: "center", backgroundColor: "#F7D6E0"}; + let bgYellow = {textAlign: "center", backgroundColor: "#FFF696"}; + + switch (column) { + case 'Issue ID': + case 'Title': + // console.log(rowData); + let bvlLink = this.props.baseURL + '/issue_tracker/issue/' + rowData["Issue ID"]; + result = ({rowData[column]}); + break; + case 'Status': + switch (cell) { + case 'closed': + case 'resolved': + result = ({cell}); + break; + case 'new': + case 'assigned': + case 'acknowledged': + case 'feedback': + result = ({cell}); + break; + default: + result = ({cell}); + break; + } + break; + case 'Description': + let a = cell.replace(/"/g,'"'); + result = ({a}); + break; + default: + result = ({cell}); + break; + } + return result; + } + + /** + * @return {JSX} the incomplete form to render. + */ + render() { + // Waiting for async data to load. + if (!this.state.isLoaded) { + return ; + } + + // The fields configured for display/hide. + let fields = [ + { + label: "Issue ID", + show: false, + type: 'text', + }, + { + label: "Title", + show: true, + type: 'text', + filter: { + name: 'Title', + type: 'text', + } + }, + { + label: "Status", + show: true, + type: 'text', + filter: { + name: 'Status', + type: 'text', + } + }, + { + label: "Date Created", + show: true, + type: 'text', + filter: { + name: 'Date Created', + type: 'text', + } + }, + { + label: "Description", + show: true, + type: 'text', + filter: { + name: 'Description', + type: 'text', + } + }, + ]; + + return ( +
+ {this.state.data == null + ? (
+ Error: no issues found. +
) + : <> +
+
+ Note: Only the last 10.000 issues will be + displayed. If more are required, please check the issue tracker. +
+ + + } +
+ ); + } +} + +IssuesViewer.propTypes = { + data: PropTypes.object, + baseURL: PropTypes.string.isRequired, + moduleURL: PropTypes.string.isRequired, +}; + +export default IssuesViewer; \ No newline at end of file diff --git a/modules/redcap/jsx/tabs/notificationViewer.js b/modules/redcap/jsx/tabs/notificationViewer.js new file mode 100644 index 00000000000..35329043c09 --- /dev/null +++ b/modules/redcap/jsx/tabs/notificationViewer.js @@ -0,0 +1,196 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import Loader from 'jsx/Loader'; +import FilterableDataTable from 'jsx/FilterableDataTable'; + + +class NotificationViewer extends Component { + + /** + * Constructor of component + * + * @param {object} props - the component properties. + */ + constructor(props) { + super(props); + + this.state = { + baseURL: props.baseURL, + data: {}, + error: false, + isLoaded: false, + }; + + this.fetchData = this.fetchData.bind(this); + this.formatColumn = this.formatColumn.bind(this); + } + + /** + * Called by React when the component has been rendered on the page. + */ + componentDidMount() { + this.fetchData(); + } + + /** + * Retrieve data for the form and save it in state. + */ + fetchData() { + fetch(`${this.state.baseURL}/?format=json`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + }).then((resp) => { + if (!resp.ok) { + this.setState({error: true}); + console.error(resp.status + ': ' + resp.statusText); + return; + } + + resp.json() + .then((data) => { + this.setState({ + data, + isLoaded: true, + }); + }); + + }).catch((error) => { + this.setState({error: true}); + console.error(error); + }); + } + + /** + * Modify behaviour of specified column cells in the Data Table component + * + * @param {string} column - column name + * @param {string} cell - cell content + * @param {object} row - row content indexed by column + * + * @return {*} a formatted table cell for a given column + */ + formatColumn(column, cell, row) { + let result = ({cell}); + + // background color + let bgGreen = {textAlign: "center", backgroundColor: "#EDF7E5"}; + let bgRed = {textAlign: "center", backgroundColor: "#F7D6E0"}; + + switch (column) { + case 'Complete': + // yes/no answers + result = (cell === '2') + ? (✅) + : (❌); + break; + default: + result = ({cell}); + break; + } + return result; + } + + /** + * @return {JSX} the incomplete form to render. + */ + render() { + // Waiting for async data to load. + if (!this.state.isLoaded) { + return ; + } + + // The fields configured for display/hide. + /** + * Fields. + */ + const fields = [ + { + label: 'Notification ID', + show: false, + type: 'text', + }, + { + label: 'Complete', + show: true, + type: 'text', + }, + { + label: 'Record ID', + show: true, + type: 'text', + filter: { + name: 'Record ID', + type: 'text', + } + }, + { + label: 'Event Name', + show: true, + type: 'text', + filter: { + name: 'Event Name', + type: 'text', + } + }, + { + label: 'Instrument', + show: true, + type: 'text', + filter: { + name: 'Instrument', + type: 'text', + } + }, + { + label: 'REDCap URL', + show: true, + type: 'text', + }, + { + label: 'Project ID', + show: true, + type: 'text', + filter: { + name: 'Project ID', + type: 'text', + } + }, + { + label: 'Received', + show: true, + type: 'date', + }, + { + label: 'Handled', + show: true, + type: 'date', + }, + ]; + + return ( + <> +
+
+ Note: Only the last 10.000 notifications will be + displayed. If more are required, please contact the administrator. +
+ + + ); + } +} + +NotificationViewer.propTypes = { + data: PropTypes.object, + baseURL: PropTypes.string.isRequired, +}; + +export default NotificationViewer; \ No newline at end of file From 3758ac591e4f60f2cc3511a3bd8402830de9871e Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 10:22:58 -0400 Subject: [PATCH 11/33] build instruction --- webpack.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webpack.config.ts b/webpack.config.ts index 7dbaa0e968b..1727aa65e93 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -83,6 +83,12 @@ const lorisModules: Record = { schedule_module: ['scheduleIndex'], api_docs: ['swagger-ui_custom'], dashboard: ['welcome'], + redcap: [ + 'redcapIndex', + 'tabs/dictionaryViewer', + 'tabs/notificationViewer', + 'tabs/issuesViewer', + ], }; /* From 88407a04c554c2df58884326451d436911e99ec0 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 11:44:37 -0400 Subject: [PATCH 12/33] lint --- modules/redcap/php/dictionary.class.inc | 19 ++++---- modules/redcap/php/issues.class.inc | 19 ++++---- modules/redcap/php/queries.class.inc | 2 +- modules/redcap/php/redcap.class.inc | 11 +++-- .../redcap/php/redcapprovisioner.class.inc | 26 ++++++----- modules/redcap/php/redcaprow.class.inc | 27 ++++++++--- modules/redcap/tools/redcap2linst.php | 46 ++++++++++--------- 7 files changed, 86 insertions(+), 64 deletions(-) diff --git a/modules/redcap/php/dictionary.class.inc b/modules/redcap/php/dictionary.class.inc index 1d63b4561c9..d21bef5930d 100644 --- a/modules/redcap/php/dictionary.class.inc +++ b/modules/redcap/php/dictionary.class.inc @@ -1,11 +1,12 @@ - + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ namespace LORIS\redcap; @@ -18,11 +19,11 @@ use Psr\Http\Message\ServerRequestInterface; * * PHP Version 8 * - * @category Behavioural - * @package Main - * @author Loris team - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ + * @category REDCap + * @package Main + * @author Regis Ongaro-Carcy + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ class Dictionary extends \NDB_Page { diff --git a/modules/redcap/php/issues.class.inc b/modules/redcap/php/issues.class.inc index 1d149360da0..9afe8e041ef 100644 --- a/modules/redcap/php/issues.class.inc +++ b/modules/redcap/php/issues.class.inc @@ -1,11 +1,12 @@ - + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ namespace LORIS\redcap; @@ -18,11 +19,11 @@ use Psr\Http\Message\ServerRequestInterface; * * PHP Version 8 * - * @category Behavioural - * @package Main - * @author Loris team - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ + * @category REDCap + * @package Main + * @author Regis Ongaro-Carcy + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ class Issues extends \NDB_Page { diff --git a/modules/redcap/php/queries.class.inc b/modules/redcap/php/queries.class.inc index 911a6d05bd1..0b24ed3abea 100644 --- a/modules/redcap/php/queries.class.inc +++ b/modules/redcap/php/queries.class.inc @@ -390,7 +390,7 @@ class Queries 'redcap_dictionary', $dictionary_entry->toDatabaseArray(), [ - "FormName" => $dictionary_entry->form_name, + "FormName" => $dictionary_entry->form_name, "FieldName" => $dictionary_entry->field_name ] ); diff --git a/modules/redcap/php/redcap.class.inc b/modules/redcap/php/redcap.class.inc index c4dda3bd283..68e2818bb1b 100644 --- a/modules/redcap/php/redcap.class.inc +++ b/modules/redcap/php/redcap.class.inc @@ -1,13 +1,14 @@ - - * @license Loris license - * @link https://www.github.com/aces/Loris + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ + namespace LORIS\redcap; /** @@ -16,8 +17,8 @@ namespace LORIS\redcap; * @category REDCap * @package Main * @author Regis Ongaro-Carcy - * @license Loris license - * @link https://www.github.com/aces/Loris + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ class REDCap extends \DataFrameworkMenu { diff --git a/modules/redcap/php/redcapprovisioner.class.inc b/modules/redcap/php/redcapprovisioner.class.inc index 4e503006648..a22e423d9eb 100644 --- a/modules/redcap/php/redcapprovisioner.class.inc +++ b/modules/redcap/php/redcapprovisioner.class.inc @@ -1,15 +1,15 @@ - + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ namespace LORIS\redcap; @@ -20,16 +20,18 @@ namespace LORIS\redcap; * * PHP Version 8 * - * @category Behavioural - * @package Main - * @subpackage Tools - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ + * @category REDCap + * @package Main + * @author Regis Ongaro-Carcy + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ class REDCapProvisioner extends \LORIS\Data\Provisioners\DBRowProvisioner { /** * Create a REDCapProvisioner. + * + * @param \LORIS\LorisInstance $loris the loris instance */ public function __construct(\LORIS\LorisInstance $loris) { diff --git a/modules/redcap/php/redcaprow.class.inc b/modules/redcap/php/redcaprow.class.inc index beeede88a03..87934c6e58e 100644 --- a/modules/redcap/php/redcaprow.class.inc +++ b/modules/redcap/php/redcaprow.class.inc @@ -1,14 +1,27 @@ - + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ + namespace LORIS\redcap; /** * An REDCap row represents a row in the REDCap menu table. * - * @category Behavioural - * @package Main - * @subpackage Tools - * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 - * @link https://www.github.com/aces/Loris/ + * @category REDCap + * @package Main + * @author Regis Ongaro-Carcy + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ */ class RedcapRow implements \LORIS\Data\DataInstance { @@ -20,7 +33,7 @@ class RedcapRow implements \LORIS\Data\DataInstance * Create a new redcap row * * @param array $row The row (in the same format as \Database::pselectRow - * returns.) + * returns.) */ public function __construct(array $row) { diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index 724f39ea003..fe0dd638b58 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -16,19 +16,19 @@ * @link https://www.github.com/aces/Loris/ */ - require_once __DIR__ . "/../../../tools/generic_includes.php"; +require_once __DIR__ . "/../../../tools/generic_includes.php"; // load redcap module - try { - $lorisInstance->getModule('redcap')->registerAutoloader(); - } catch (\LorisNoSuchModuleException $th) { - error_log("[error] no 'redcap' module found."); - exit(1); - } +try { + $lorisInstance->getModule('redcap')->registerAutoloader(); +} catch (\LorisNoSuchModuleException $th) { + error_log("[error] no 'redcap' module found."); + exit(1); +} -ini_set('display_errors', 1); -ini_set('display_startup_errors', 1); -error_reporting(E_ALL); +// ini_set('display_errors', 1); +// ini_set('display_startup_errors', 1); +// error_reporting(E_ALL); use LORIS\redcap\client\RedcapHttpClient; use LORIS\redcap\client\models\RedcapDictionaryRecord; @@ -81,7 +81,7 @@ $linst = $dict_record->toLINST(); if (!empty($linst)) { $instruments[$dict_record->form_name][] = $linst; - $dictionary[$dict_record->form_name][] = $dict_record; + $dictionary[$dict_record->form_name][] = $dict_record; } } @@ -104,7 +104,7 @@ // update the db redcap_dictionary table per instrument fwrite(STDOUT, "\n-- Writing entries to database 'redcap_dictionary' table.\n\n"); -$queries = new Queries($lorisInstance); +$queries = new Queries($lorisInstance); $nbEntriesCount = [ 'created' => 0, 'updated' => 0, @@ -118,8 +118,8 @@ ); // update count - $nbEntriesCount['created'] += $nbEntries['created']; - $nbEntriesCount['updated'] += $nbEntries['updated']; + $nbEntriesCount['created'] += $nbEntries['created']; + $nbEntriesCount['updated'] += $nbEntries['updated']; $nbEntriesCount['untouched'] += $nbEntries['untouched']; } @@ -152,7 +152,7 @@ function updateREDCapDictionaryInstrumentEntries( $instrumentExists = $queries->instrumentExistsInREDCapDictionary( $instrument_name ); - $updateMsg = $instrumentExists ? "updating" : "creating"; + $updateMsg = $instrumentExists ? "updating" : "creating"; fwrite(STDOUT, " -> {$updateMsg} '{$instrument_name}'\n"); // count @@ -179,12 +179,16 @@ function updateREDCapDictionaryInstrumentEntries( // escape HTML from this entry to compare // same method use in Database class -> update - $escaped_props = array_map(fn($v) => is_string($v) ? htmlspecialchars( - $v, - ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, - 'UTF-8', - false - ) : $v, $redcap_dictionary_entry->toArray()); + $escaped_props = array_map( + fn($v) => is_string($v) + ? htmlspecialchars( + $v, + ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, + 'UTF-8', + false + ) + : $v, + $redcap_dictionary_entry->toArray()); $redcap_escaped_entry = new RedcapDictionaryRecord( array_combine( RedcapDictionaryRecord::getHeaders(), From 7d1b6f673880bfa7a3c02711af3dd893455103ed Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 11:51:54 -0400 Subject: [PATCH 13/33] lint --- modules/redcap/php/dictionary.class.inc | 3 ++- modules/redcap/php/issues.class.inc | 3 ++- modules/redcap/php/redcap.class.inc | 3 ++- modules/redcap/php/redcapprovisioner.class.inc | 1 + modules/redcap/php/redcaprow.class.inc | 1 + modules/redcap/tools/redcap2linst.php | 3 ++- 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/redcap/php/dictionary.class.inc b/modules/redcap/php/dictionary.class.inc index d21bef5930d..12113ecc645 100644 --- a/modules/redcap/php/dictionary.class.inc +++ b/modules/redcap/php/dictionary.class.inc @@ -1,4 +1,5 @@ array_values($v), - iterator_to_array($dictionary->getIterator()) + iterator_to_array($dictionary->getIterator()) ) ); } diff --git a/modules/redcap/php/issues.class.inc b/modules/redcap/php/issues.class.inc index 9afe8e041ef..d2297e48085 100644 --- a/modules/redcap/php/issues.class.inc +++ b/modules/redcap/php/issues.class.inc @@ -1,4 +1,5 @@ array_values($v), - iterator_to_array($issues->getIterator()) + iterator_to_array($issues->getIterator()) ) ); } diff --git a/modules/redcap/php/redcap.class.inc b/modules/redcap/php/redcap.class.inc index 68e2818bb1b..9b1b0b14986 100644 --- a/modules/redcap/php/redcap.class.inc +++ b/modules/redcap/php/redcap.class.inc @@ -1,4 +1,5 @@ hasAnyPermission( [ diff --git a/modules/redcap/php/redcapprovisioner.class.inc b/modules/redcap/php/redcapprovisioner.class.inc index a22e423d9eb..e2877a17b0e 100644 --- a/modules/redcap/php/redcapprovisioner.class.inc +++ b/modules/redcap/php/redcapprovisioner.class.inc @@ -1,4 +1,5 @@ toArray()); + $redcap_dictionary_entry->toArray() + ); $redcap_escaped_entry = new RedcapDictionaryRecord( array_combine( RedcapDictionaryRecord::getHeaders(), From 002c68d4f0bb006d12ad28e1710a3cd572d4d606 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 11:58:18 -0400 Subject: [PATCH 14/33] lint --- modules/redcap/tools/redcap2linst.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index 0a37fc9384b..4cea068da26 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -179,16 +179,17 @@ function updateREDCapDictionaryInstrumentEntries( // escape HTML from this entry to compare // same method use in Database class -> update + $cleanHTML = fn($v) => is_string($v) + ? htmlspecialchars( + $v, + ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, + 'UTF-8', + false + ) + : $v; $escaped_props = array_map( - fn($v) => is_string($v) - ? htmlspecialchars( - $v, - ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, - 'UTF-8', - false - ) - : $v, - $redcap_dictionary_entry->toArray() + $cleanHTML, + $redcap_dictionary_entry->toArray() ); $redcap_escaped_entry = new RedcapDictionaryRecord( array_combine( From fb93d42cdd555b3f010bd5e1a6e031fa761165a7 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 12:12:22 -0400 Subject: [PATCH 15/33] lint --- modules/redcap/tools/redcap2linst.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index 4cea068da26..c83503ada93 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -162,6 +162,16 @@ function updateREDCapDictionaryInstrumentEntries( 'untouched' => 0 ]; + // closure for escaped html + $cleanHTML = fn($v) => is_string($v) + ? htmlspecialchars( + $v, + ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, + 'UTF-8', + false + ) + : $v; + // go through entries for this instrument foreach ($redcap_dictionary as $redcap_dictionary_entry) { // get the db correspoding field name @@ -179,14 +189,6 @@ function updateREDCapDictionaryInstrumentEntries( // escape HTML from this entry to compare // same method use in Database class -> update - $cleanHTML = fn($v) => is_string($v) - ? htmlspecialchars( - $v, - ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, - 'UTF-8', - false - ) - : $v; $escaped_props = array_map( $cleanHTML, $redcap_dictionary_entry->toArray() From eebd1f4b9b30bf174ee8fec7a01f28920824279f Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 15:41:32 -0400 Subject: [PATCH 16/33] lint --- modules/redcap/tools/redcap2linst.php | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index c83503ada93..168859d80db 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -32,6 +32,7 @@ use LORIS\redcap\client\RedcapHttpClient; use LORIS\redcap\client\models\RedcapDictionaryRecord; +use LORIS\redcap\client\models\RedcapInstrument; use LORIS\redcap\config\RedcapConfigParser; use LORIS\redcap\Queries; @@ -312,12 +313,17 @@ function getDictionaryCSVStream( function writeLINSTFile( string $output_dir, string $instrument_name, - string $instrument_title, + RedcapInstrument $instrumentObject, array $instrument ): void { fwrite(STDOUT, " -> writing '{$instrument_name}'\n"); // $fp = fopen("{$output_dir}/{$instrument_name}.linst", "w"); + + if ($fp === false) { + throw new \LorisException("Cannot open path: {$output_dir}/{$instrument_name}.linst"); + } + fwrite($fp, "{-@-}testname{@}{$instrument_name}\n"); fwrite($fp, "table{@}{$instrument_name}\n"); fwrite($fp, "title{@}{$instrument_title}\n"); @@ -396,6 +402,23 @@ function showHelp() * * @return array */ + +/** + * Check options and return a clean version. + * + * @param LORIS\LorisInstance $loris a loris instance + * @param array $options options + * + * @return array{ + * file: mixed, + * inputType: mixed, + * outputDir: mixed, + * redcapConnection: RedcapHttpClient, + * redcapInstance: mixed, + * redcapProject: mixed, + * trimInstrumentName: bool + * } + */ function checkOptions(\LORIS\LorisInstance $loris, array &$options): array { // ouput dir @@ -473,7 +496,7 @@ function checkOptions(\LORIS\LorisInstance $loris, array &$options): array ); // trim instrument name - $trim_name = isset($options['t']) || isset($options['trim-formname']) || false; + $trim_name = isset($options['trim-formname']) || false; // checked and clean options return [ From 3b4c6f5ae9674cfceeae71a381a1a1edf28c0011 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 15:51:33 -0400 Subject: [PATCH 17/33] lint --- modules/redcap/tools/redcap2linst.php | 39 +++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index 168859d80db..f9bb6840656 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -94,12 +94,11 @@ // write instrument fwrite(STDOUT, "\n-- Writing LINST/META files.\n\n"); -foreach ($instruments as $instrument_name => $instrument) { +foreach ($instruments as $instrument_name => $fields) { writeLINSTFile( $options['outputDir'], - $instrument_name, $redcap_intruments_map[$instrument_name], - $instrument + $fields ); } @@ -303,30 +302,30 @@ function getDictionaryCSVStream( /** * Write LINST file and its associated META file. * - * @param string $output_dir the output directory - * @param string $instrument_name the instrument name - * @param string $instrument_title the instrument title/label - * @param array $instrument the instrument data + * @param string $output_dir the output directory + * @param RedcapInstrument $instrument the instrument object + * @param array $fields the instrument data * * @return void */ function writeLINSTFile( string $output_dir, - string $instrument_name, - RedcapInstrument $instrumentObject, - array $instrument + RedcapInstrument $instrument, + array $fields ): void { - fwrite(STDOUT, " -> writing '{$instrument_name}'\n"); + fwrite(STDOUT, " -> writing '{$instrument->name}'\n"); // - $fp = fopen("{$output_dir}/{$instrument_name}.linst", "w"); + $fp = fopen("{$output_dir}/{$instrument->name}.linst", "w"); if ($fp === false) { - throw new \LorisException("Cannot open path: {$output_dir}/{$instrument_name}.linst"); + throw new \LorisException( + "Cannot open path: {$output_dir}/{$instrument->name}.linst" + ); } - fwrite($fp, "{-@-}testname{@}{$instrument_name}\n"); - fwrite($fp, "table{@}{$instrument_name}\n"); - fwrite($fp, "title{@}{$instrument_title}\n"); + fwrite($fp, "{-@-}testname{@}{$instrument->name}\n"); + fwrite($fp, "table{@}{$instrument->name}\n"); + fwrite($fp, "title{@}{$instrument->label}\n"); // Standard LORIS metadata fields that the instrument builder adds // and LINST class automatically adds to instruments. @@ -336,7 +335,7 @@ function writeLINSTFile( fwrite($fp, "static{@}Window_Difference{@}Window Difference (+/- Days)\n"); fwrite($fp, "select{@}Examiner{@}Examiner{@}NULL=>''\n"); - foreach ($instrument as $field) { + foreach ($fields as $field) { // avoid 'timestamp_start', changed in 'static' instead of 'text' if (str_contains($field, "{@}timestamp_start{@}")) { // transform timestamp start to static @@ -354,9 +353,9 @@ function writeLINSTFile( fclose($fp); // META file - $fp_meta = fopen("{$output_dir}/{$instrument_name}.meta", "w"); - fwrite($fp_meta, "testname{@}{$instrument_name}\n"); - fwrite($fp_meta, "table{@}{$instrument_name}\n"); + $fp_meta = fopen("{$output_dir}/{$instrument->name}.meta", "w"); + fwrite($fp_meta, "testname{@}{$instrument->name}\n"); + fwrite($fp_meta, "table{@}{$instrument->name}\n"); fwrite($fp_meta, "jsondata{@}true\n"); fwrite($fp_meta, "norules{@}true"); fclose($fp_meta); From f658b492a306aaa96726b356e2a123a48da47456 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 15:52:21 -0400 Subject: [PATCH 18/33] lint --- modules/redcap/tools/redcap2linst.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index f9bb6840656..6a3d95d98cb 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -405,7 +405,7 @@ function showHelp() /** * Check options and return a clean version. * - * @param LORIS\LorisInstance $loris a loris instance + * @param LORIS\LorisInstance $loris a loris instance * @param array $options options * * @return array{ From 9b7a194af7dda6ff9fb8c944f2d48c7c82421663 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 16:06:27 -0400 Subject: [PATCH 19/33] file open check --- modules/redcap/tools/redcap2linst.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index 6a3d95d98cb..2517e1cecab 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -354,6 +354,13 @@ function writeLINSTFile( // META file $fp_meta = fopen("{$output_dir}/{$instrument->name}.meta", "w"); + + if ($fp_meta === false) { + throw new \LorisException( + "Cannot open path: {$output_dir}/{$instrument->name}.meta" + ); + } + fwrite($fp_meta, "testname{@}{$instrument->name}\n"); fwrite($fp_meta, "table{@}{$instrument->name}\n"); fwrite($fp_meta, "jsondata{@}true\n"); From d86655c7ed94a3f3f61aadc5f7b289bb9808e12f Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 16:06:35 -0400 Subject: [PATCH 20/33] redcap type check --- modules/redcap/tools/redcap2linst.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index 2517e1cecab..e8c1f6ad4a2 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -416,12 +416,12 @@ function showHelp() * @param array $options options * * @return array{ - * file: mixed, - * inputType: mixed, - * outputDir: mixed, + * file: string, + * inputType: string, + * outputDir: string, * redcapConnection: RedcapHttpClient, - * redcapInstance: mixed, - * redcapProject: mixed, + * redcapInstance: string, + * redcapProject: string, * trimInstrumentName: bool * } */ From efcb56fd27cda13f65459436c723abeef56c557b Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 16:18:53 -0400 Subject: [PATCH 21/33] redcap type check --- modules/redcap/tools/redcap2linst.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index e8c1f6ad4a2..1414016037f 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -416,8 +416,8 @@ function showHelp() * @param array $options options * * @return array{ - * file: string, - * inputType: string, + * file: string|null, + * inputType: 'api'|'file', * outputDir: string, * redcapConnection: RedcapHttpClient, * redcapInstance: string, @@ -429,7 +429,7 @@ function checkOptions(\LORIS\LorisInstance $loris, array &$options): array { // ouput dir $output_dir = $options['o'] ?? $options['output-dir'] ?? null; - if (empty($output_dir)) { + if ($output_dir === null || empty($output_dir)) { fprintf(STDERR, "Output directory required.\n"); showHelp(); exit(1); @@ -477,12 +477,12 @@ function checkOptions(\LORIS\LorisInstance $loris, array &$options): array $redcap_instance = $options['r'] ?? $options['redcap-instance'] ?? null; $redcap_project = $options['p'] ?? $options['redcap-project'] ?? null; - if (empty($redcap_instance)) { + if ($redcap_instance === null || empty($redcap_instance)) { fprintf(STDERR, "REDCap instance name required.\n"); showHelp(); exit(1); } - if (empty($redcap_project)) { + if ($redcap_project === null || empty($redcap_project)) { fprintf(STDERR, "REDCap project ID required.\n"); showHelp(); exit(1); From 312dbc8699e98a72dd808fdb03235532a800f2a5 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 11 Jul 2025 16:24:06 -0400 Subject: [PATCH 22/33] redcap type check --- modules/redcap/tools/redcap2linst.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/redcap/tools/redcap2linst.php b/modules/redcap/tools/redcap2linst.php index 1414016037f..797012be592 100644 --- a/modules/redcap/tools/redcap2linst.php +++ b/modules/redcap/tools/redcap2linst.php @@ -434,6 +434,10 @@ function checkOptions(\LORIS\LorisInstance $loris, array &$options): array showHelp(); exit(1); } + if (!is_string($output_dir)) { + fprintf(STDERR, "Output directory '{$output_dir}' must be a path.\n"); + exit(1); + } if (!is_dir($output_dir)) { fprintf(STDERR, "Output directory '{$output_dir}' does not exist.\n"); exit(1); From 9f70294b069464c63c1132cdd5951cb08ff19057 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 31 Jul 2025 11:53:36 -0400 Subject: [PATCH 23/33] FormName to InstrumentName --- SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql | 2 +- modules/redcap/php/dictionary.class.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql b/SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql index 47f8a4cc9bd..1d4534695b5 100644 --- a/SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql +++ b/SQL/New_patches/2025-07-10_REDCap_front_end_permission.sql @@ -16,7 +16,7 @@ INSERT INTO perm_perm_action_rel (permID, actionID) -- adds the redcap dictionary table CREATE TABLE `redcap_dictionary` ( ID int(10) unsigned NOT NULL auto_increment, - FormName varchar(255), + InstrumentName varchar(255), FieldName varchar(255) NOT NULL, FieldRequired boolean NOT NULL, FieldType varchar(20) NOT NULL, diff --git a/modules/redcap/php/dictionary.class.inc b/modules/redcap/php/dictionary.class.inc index 12113ecc645..b23a50cd3bb 100644 --- a/modules/redcap/php/dictionary.class.inc +++ b/modules/redcap/php/dictionary.class.inc @@ -85,7 +85,7 @@ class Dictionary extends \NDB_Page $db = $this->loris->getDatabaseConnection(); $dictionary = $db->pselect( "SELECT - FormName, + InstrumentName, FieldName, FieldRequired, FieldType, From 9d06c96f7dfb95de550c138320ec60f6bd72c524 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 12 Dec 2025 14:13:17 -0500 Subject: [PATCH 24/33] i18n redcap compile cmd --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 5908d35ed1e..61fe245c35a 100755 --- a/Makefile +++ b/Makefile @@ -262,3 +262,6 @@ electrophysiology_browser: $(filter modules/electrophysiology_browser/%,$(MOFILE dicom_archive: $(filter modules/dicom_archive/%,$(MOFILES)) $(filter modules/dicom_archive/%,$(I18NJSONFILES)) target=dicom_archive npm run compile + +redcap: $(filter modules/redcap/%,$(MOFILES)) $(filter modules/redcap/%,$(I18NJSONFILES)) + target=redcap npm run compile \ No newline at end of file From 6e0eac2d1cc6b46f48ffabac4b0502c82f6ef785 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 12 Dec 2025 14:13:57 -0500 Subject: [PATCH 25/33] redcap dictionary - form name to instrument name --- SQL/0000-00-00-schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SQL/0000-00-00-schema.sql b/SQL/0000-00-00-schema.sql index 1a61e0dbcf7..7e4d8658c78 100644 --- a/SQL/0000-00-00-schema.sql +++ b/SQL/0000-00-00-schema.sql @@ -2711,7 +2711,7 @@ CREATE TABLE `redcap_notification` ( CREATE TABLE `redcap_dictionary` ( ID int(10) unsigned NOT NULL auto_increment, - FormName varchar(255), + InstrumentName varchar(255), FieldName varchar(255) NOT NULL, FieldRequired boolean NOT NULL, FieldType varchar(20) NOT NULL, From b9a60e179496f844a57f52591684a530323ffad4 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 12 Dec 2025 14:14:42 -0500 Subject: [PATCH 26/33] redcap permission ui view --- SQL/0000-00-02-Permission.sql | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/SQL/0000-00-02-Permission.sql b/SQL/0000-00-02-Permission.sql index 746c66ac718..3234a6e7632 100644 --- a/SQL/0000-00-02-Permission.sql +++ b/SQL/0000-00-02-Permission.sql @@ -277,11 +277,6 @@ INSERT INTO `perm_perm_action_rel` (permID, actionID) VALUES ((SELECT permID FROM permissions WHERE code = 'imaging_uploader_nosessionid'),1), ((SELECT permID FROM permissions WHERE code = 'dicom_archive_nosessionid'),1), ((SELECT permID FROM permissions WHERE code = 'dicom_archive_view_ownsites'),1), -<<<<<<< HEAD - ((SELECT permID FROM permissions WHERE code = 'view_instrument_data'),1), - ((SELECT permID FROM permissions WHERE code = 'redcap_ui_view'),1) - ; -======= ((SELECT permID FROM permissions WHERE code = 'biobank_specimen_view'),1), ((SELECT permID FROM permissions WHERE code = 'biobank_specimen_create'),2), ((SELECT permID FROM permissions WHERE code = 'biobank_specimen_update'),3), @@ -295,8 +290,8 @@ INSERT INTO `perm_perm_action_rel` (permID, actionID) VALUES ((SELECT permID FROM permissions WHERE code = 'biobank_fullprojectaccess'),1), ((SELECT permID FROM permissions WHERE code = 'view_instrument_data'),1), ((SELECT permID FROM permissions WHERE code = 'dqt_view'),1), - ((SELECT permID FROM permissions WHERE code = 'dqt_view'),8); ->>>>>>> main + ((SELECT permID FROM permissions WHERE code = 'dqt_view'),8), + ((SELECT permID FROM permissions WHERE code = 'redcap_ui_view'),1); -- permissions for each notification module From ad0dc1f652a3e55c59d2c5fcb7f71d68327a656b Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 12 Dec 2025 14:17:57 -0500 Subject: [PATCH 27/33] redcap - raisinbread --- raisinbread/RB_files/RB_modules.sql | 5 +---- raisinbread/RB_files/RB_permissions.sql | 2 +- raisinbread/RB_files/RB_user_perm_rel.sql | 1 + 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/raisinbread/RB_files/RB_modules.sql b/raisinbread/RB_files/RB_modules.sql index e4f77a847d9..734c7fe88a7 100644 --- a/raisinbread/RB_files/RB_modules.sql +++ b/raisinbread/RB_files/RB_modules.sql @@ -50,11 +50,8 @@ INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (47,'electrophysiology_upl INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (48,'schedule_module','Y'); INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (49,'dataquery','Y'); INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (50,'oidc','N'); -<<<<<<< HEAD -INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (51,'redcap','N'); -======= INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (51,'policy_tracker','Y'); INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (52,'biobank','Y'); ->>>>>>> main +INSERT INTO `modules` (`ID`, `Name`, `Active`) VALUES (53,'redcap','N'); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_permissions.sql b/raisinbread/RB_files/RB_permissions.sql index 174f9a90c4a..2e560c714d5 100644 --- a/raisinbread/RB_files/RB_permissions.sql +++ b/raisinbread/RB_files/RB_permissions.sql @@ -81,6 +81,6 @@ INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categor INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (87,'biobank_container_view','View Containers',51,2); INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (88,'biobank_pool_view','View Pools',51,2); INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (89,'dqt_view','Cross-Modality Data (legacy)',44,2); -INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (90,'redcap_ui_view','REDCap GUI - View',51,2); +INSERT INTO `permissions` (`permID`, `code`, `description`, `moduleID`, `categoryID`) VALUES (90,'redcap_ui_view','REDCap GUI - View',53,2); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; diff --git a/raisinbread/RB_files/RB_user_perm_rel.sql b/raisinbread/RB_files/RB_user_perm_rel.sql index b91066a1c14..7191d635254 100644 --- a/raisinbread/RB_files/RB_user_perm_rel.sql +++ b/raisinbread/RB_files/RB_user_perm_rel.sql @@ -80,5 +80,6 @@ INSERT INTO `user_perm_rel` (`userID`, `permID`) VALUES (1,83); INSERT INTO `user_perm_rel` (`userID`, `permID`) VALUES (1,84); INSERT INTO `user_perm_rel` (`userID`, `permID`) VALUES (1,85); INSERT INTO `user_perm_rel` (`userID`, `permID`) VALUES (1,86); +INSERT INTO `user_perm_rel` (`userID`, `permID`) VALUES (1,90); UNLOCK TABLES; SET FOREIGN_KEY_CHECKS=1; From 5f1f7744eb2ca0a00a31f876b74215b2a90ace01 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 12 Dec 2025 14:36:56 -0500 Subject: [PATCH 28/33] lint --- modules/redcap/php/dictionary.class.inc | 2 +- modules/redcap/php/issues.class.inc | 2 +- modules/redcap/php/redcap.class.inc | 2 +- modules/redcap/php/redcapprovisioner.class.inc | 2 +- modules/redcap/php/redcaprow.class.inc | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/redcap/php/dictionary.class.inc b/modules/redcap/php/dictionary.class.inc index b23a50cd3bb..4353414ff10 100644 --- a/modules/redcap/php/dictionary.class.inc +++ b/modules/redcap/php/dictionary.class.inc @@ -118,4 +118,4 @@ class Dictionary extends \NDB_Page ) ); } -} \ No newline at end of file +} diff --git a/modules/redcap/php/issues.class.inc b/modules/redcap/php/issues.class.inc index d2297e48085..7b6de60d4ae 100644 --- a/modules/redcap/php/issues.class.inc +++ b/modules/redcap/php/issues.class.inc @@ -119,4 +119,4 @@ class Issues extends \NDB_Page ) ); } -} \ No newline at end of file +} diff --git a/modules/redcap/php/redcap.class.inc b/modules/redcap/php/redcap.class.inc index 9b1b0b14986..075c8505318 100644 --- a/modules/redcap/php/redcap.class.inc +++ b/modules/redcap/php/redcap.class.inc @@ -96,4 +96,4 @@ class REDCap extends \DataFrameworkMenu [$baseurl . "/redcap/js/redcapIndex.js"] ); } -} \ No newline at end of file +} diff --git a/modules/redcap/php/redcapprovisioner.class.inc b/modules/redcap/php/redcapprovisioner.class.inc index e2877a17b0e..c0a1b3bba99 100644 --- a/modules/redcap/php/redcapprovisioner.class.inc +++ b/modules/redcap/php/redcapprovisioner.class.inc @@ -69,4 +69,4 @@ class REDCapProvisioner extends \LORIS\Data\Provisioners\DBRowProvisioner { return new RedcapRow($row); } -} \ No newline at end of file +} diff --git a/modules/redcap/php/redcaprow.class.inc b/modules/redcap/php/redcaprow.class.inc index 1536559f656..50e2bd36361 100644 --- a/modules/redcap/php/redcaprow.class.inc +++ b/modules/redcap/php/redcaprow.class.inc @@ -50,4 +50,4 @@ class RedcapRow implements \LORIS\Data\DataInstance { return $this->DBRow; } -} \ No newline at end of file +} From 5c1ba66aa3786b747fef472bac197ea52366336f Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 12 Dec 2025 16:24:15 -0500 Subject: [PATCH 29/33] redcap2linst in generic tools --- {modules/redcap/tools => tools}/redcap2linst.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {modules/redcap/tools => tools}/redcap2linst.php (100%) diff --git a/modules/redcap/tools/redcap2linst.php b/tools/redcap2linst.php similarity index 100% rename from modules/redcap/tools/redcap2linst.php rename to tools/redcap2linst.php From cab539d08c19c286568248aaf63f7ff129671270 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 12 Dec 2025 16:25:14 -0500 Subject: [PATCH 30/33] generic_include path --- tools/redcap2linst.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/redcap2linst.php b/tools/redcap2linst.php index 7e2d7e1ec74..c30f8458125 100644 --- a/tools/redcap2linst.php +++ b/tools/redcap2linst.php @@ -16,7 +16,7 @@ * @link https://www.github.com/aces/Loris/ */ -require_once __DIR__ . "/../../../tools/generic_includes.php"; +require_once __DIR__ . "/../tools/generic_includes.php"; // load redcap module try { From b046f94ae81af7ca6b71c17409c9b5d7b1d5ec22 Mon Sep 17 00:00:00 2001 From: regisoc Date: Mon, 15 Dec 2025 10:11:52 -0500 Subject: [PATCH 31/33] remove request parameters from get in dictionary and issues classes --- modules/redcap/php/dictionary.class.inc | 6 ++---- modules/redcap/php/issues.class.inc | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/modules/redcap/php/dictionary.class.inc b/modules/redcap/php/dictionary.class.inc index 4353414ff10..1767bbe94f2 100644 --- a/modules/redcap/php/dictionary.class.inc +++ b/modules/redcap/php/dictionary.class.inc @@ -66,7 +66,7 @@ class Dictionary extends \NDB_Page public function handle(ServerRequestInterface $request) : ResponseInterface { return match ($request->getMethod()) { - 'GET' => $this->_handleGET($request), + 'GET' => $this->_handleGET(), default => new \LORIS\Http\Response\JSON\MethodNotAllowed( $this->allowedMethods() ), @@ -76,11 +76,9 @@ class Dictionary extends \NDB_Page /** * Handle an incoming HTTP GET request. * - * @param ServerRequestInterface $request The incoming PSR7 request - * * @return ResponseInterface */ - private function _handleGET(ServerRequestInterface $request) : ResponseInterface + private function _handleGET() : ResponseInterface { $db = $this->loris->getDatabaseConnection(); $dictionary = $db->pselect( diff --git a/modules/redcap/php/issues.class.inc b/modules/redcap/php/issues.class.inc index 7b6de60d4ae..91adb3c17a9 100644 --- a/modules/redcap/php/issues.class.inc +++ b/modules/redcap/php/issues.class.inc @@ -68,7 +68,7 @@ class Issues extends \NDB_Page // Ensure GET or POST request. switch ($request->getMethod()) { case 'GET': - return $this->_handleGET($request); + return $this->_handleGET(); default: return new \LORIS\Http\Response\JSON\MethodNotAllowed( $this->allowedMethods() @@ -79,11 +79,9 @@ class Issues extends \NDB_Page /** * Handle an incoming HTTP GET request. * - * @param ServerRequestInterface $request The incoming PSR7 request - * * @return ResponseInterface */ - private function _handleGET(ServerRequestInterface $request) : ResponseInterface + private function _handleGET(): ResponseInterface { $db = $this->loris->getDatabaseConnection(); $issues = $db->pselect( From b3febab99b6272a5e675574320785ae7b78afcdb Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 16 Dec 2025 16:42:46 -0500 Subject: [PATCH 32/33] jsx lint --- modules/redcap/jsx/redcapIndex.js | 75 ++-- modules/redcap/jsx/tabs/dictionaryViewer.js | 105 +++--- modules/redcap/jsx/tabs/issuesViewer.js | 228 +++++------ modules/redcap/jsx/tabs/notificationViewer.js | 354 +++++++++--------- 4 files changed, 378 insertions(+), 384 deletions(-) diff --git a/modules/redcap/jsx/redcapIndex.js b/modules/redcap/jsx/redcapIndex.js index f6f1181c6ba..bc96b571d2b 100644 --- a/modules/redcap/jsx/redcapIndex.js +++ b/modules/redcap/jsx/redcapIndex.js @@ -13,55 +13,54 @@ import {createRoot} from 'react-dom/client'; * @return {JSX} */ export default function RedcapIndex(props) { + // list of tabs + const tabList = [ + {id: 'notificationTable', label: 'Notifications'}, + {id: 'dictionaryTable', label: 'Data Dictionary'}, + {id: 'issuesTable', label: 'Issues'}, + ]; - // list of tabs - const tabList = [ - {id: 'notificationTable', label: 'Notifications'}, - {id: 'dictionaryTable', label: 'Data Dictionary'}, - {id: 'issuesTable', label: 'Issues'}, - ]; + return ( + <> + - return ( - <> - + {/* notifications */} + + + - {/* notifications */} - - - + {/* dictionary */} + + + - {/* dictionary */} - - - + {/* issues */} + + + - {/* issues */} - - - - - - - ); + + + ); } RedcapIndex.propTypes = { - baseURL: PropTypes.string.isRequired, - moduleURL: PropTypes.string.isRequired, - hasPermission: PropTypes.func.isRequired, + baseURL: PropTypes.string.isRequired, + moduleURL: PropTypes.string.isRequired, + hasPermission: PropTypes.func.isRequired, }; /** * Render REDCap main page on page load */ document.addEventListener('DOMContentLoaded', function() { - createRoot( - document.getElementById('lorisworkspace') - ).render( - - ); -}); \ No newline at end of file + createRoot( + document.getElementById('lorisworkspace') + ).render( + + ); +}); diff --git a/modules/redcap/jsx/tabs/dictionaryViewer.js b/modules/redcap/jsx/tabs/dictionaryViewer.js index 8db6eb843a8..05282666d89 100644 --- a/modules/redcap/jsx/tabs/dictionaryViewer.js +++ b/modules/redcap/jsx/tabs/dictionaryViewer.js @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React, {Component} from 'reac'; import PropTypes from 'prop-types'; import Loader from 'jsx/Loader'; import FilterableDataTable from 'jsx/FilterableDataTable'; @@ -7,7 +7,6 @@ import FilterableDataTable from 'jsx/FilterableDataTable'; * Dictionary viewer component */ class DictionaryViewer extends Component { - /** * Constructor of component * @@ -60,7 +59,6 @@ class DictionaryViewer extends Component { isLoaded: true, }); }); - }).catch((error) => { this.setState({error: true}); console.error(error); @@ -79,23 +77,22 @@ class DictionaryViewer extends Component { formatColumn(column, cell, rowData, rowHeaders) { let result = ({cell}); - // background color - let bgGreen = {textAlign: "center", backgroundColor: "#EDF7E5"}; - let bgRed = {textAlign: "center", backgroundColor: "#F7D6E0"}; - let bgYellow = {textAlign: "center", backgroundColor: "#FFF696"}; - - switch (column) { - case 'Required': - // yes/no answers - result = (cell === '1') - ? (✅) - : (🟨); - break; - default: - result = ({cell}); - break; - } - return result; + // background color + let bgGreen = {textAlign: 'center', backgroundColor: '#EDF7E5'}; + let bgYellow = {textAlign: 'center', backgroundColor: '#FFF696'}; + + switch (column) { + case 'Required': + // yes/no answers + result = (cell === '1') + ? (✅) + : (🟨); + break; + default: + result = ({cell}); + break; + } + return result; } /** @@ -104,126 +101,126 @@ class DictionaryViewer extends Component { render() { // Waiting for async data to load. if (!this.state.isLoaded) { - return ; + return ; } // The fields configured for display/hide. let fields = [ { - label: "Instrument", + label: 'Instrument', show: true, type: 'text', filter: { name: 'Instrument', type: 'text', - } + }, }, { - label: "Field", + label: 'Field', show: true, type: 'text', filter: { name: 'Field', type: 'text', - } + }, }, { - label: "Required", + label: 'Required', show: true, type: 'text', }, { - label: "Type", + label: 'Type', show: true, type: 'text', filter: { name: 'Type', type: 'text', - } + }, }, { - label: "Label", + label: 'Label', show: true, type: 'text', filter: { name: 'Label', type: 'text', - } + }, }, { - label: "Section Header", + label: 'Section Header', show: false, type: 'text', filter: { name: 'Section Header', type: 'text', - } + }, }, { - label: "Choices", + label: 'Choices', show: true, type: 'text', filter: { name: 'Choices', type: 'text', - } + }, }, { - label: "Note", + label: 'Note', show: true, type: 'text', filter: { name: 'Project ID', type: 'text', - } + }, }, { - label: "Validation Type", + label: 'Validation Type', show: true, type: 'text', }, { - label: "Validation Min", + label: 'Validation Min', show: true, type: 'text', }, { - label: "Validation Max", + label: 'Validation Max', show: true, type: 'text', }, { - label: "Identifier", + label: 'Identifier', show: true, type: 'text', }, { - label: "Branching Logic", + label: 'Branching Logic', show: true, type: 'text', }, { - label: "Custom Alignment", + label: 'Custom Alignment', show: true, type: 'text', }, { - label: "Question Number", + label: 'Question Number', show: true, type: 'text', }, { - label: "Matrix Group", + label: 'Matrix Group', show: true, type: 'text', }, { - label: "Matrix Ranking", + label: 'Matrix Ranking', show: true, type: 'text', }, { - label: "Annotation", + label: 'Annotation', show: true, type: 'text', }, @@ -233,14 +230,14 @@ class DictionaryViewer extends Component {
{this.state.data == null ? (
- Error: no Data Dictionary entry found. -
) + Error: no Data Dictionary entry found. +
) : + name='redcapDictionaryTable' + data={this.state.data} + fields={fields} + getFormattedCell={this.formatColumn} + /> } ); @@ -252,4 +249,4 @@ DictionaryViewer.propTypes = { baseURL: PropTypes.string.isRequired, }; -export default DictionaryViewer; \ No newline at end of file +export default DictionaryViewer; diff --git a/modules/redcap/jsx/tabs/issuesViewer.js b/modules/redcap/jsx/tabs/issuesViewer.js index a85fec4a8a2..d4272fcf32b 100644 --- a/modules/redcap/jsx/tabs/issuesViewer.js +++ b/modules/redcap/jsx/tabs/issuesViewer.js @@ -7,7 +7,6 @@ import FilterableDataTable from 'jsx/FilterableDataTable'; * Issues viewer component */ class IssuesViewer extends Component { - /** * Constructor of component * @@ -39,33 +38,32 @@ class IssuesViewer extends Component { * Retrieve data and save it in state. */ fetchData() { - fetch(`${this.state.moduleURL}/issues`, - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - } - ).then((resp) => { - if (!resp.ok) { - this.setState({error: true}); - console.error(resp.status + ': ' + resp.statusText); - return; - } - - resp.json() - .then((data) => { - this.setState({ - data, - isLoaded: true, - }); - }); - - }).catch((error) => { - this.setState({error: true}); - console.error(error); + fetch(`${this.state.moduleURL}/issues`, + { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + } + ).then((resp) => { + if (!resp.ok) { + this.setState({error: true}); + console.error(resp.status + ': ' + resp.statusText); + return; + } + + resp.json() + .then((data) => { + this.setState({ + data, + isLoaded: true, + }); }); + }).catch((error) => { + this.setState({error: true}); + console.error(error); + }); } /** @@ -80,44 +78,46 @@ class IssuesViewer extends Component { formatColumn(column, cell, rowData, rowHeaders) { let result = ({cell}); - // background color - let bgGreen = {textAlign: "center", backgroundColor: "#EDF7E5"}; - let bgRed = {textAlign: "center", backgroundColor: "#F7D6E0"}; - let bgYellow = {textAlign: "center", backgroundColor: "#FFF696"}; - - switch (column) { - case 'Issue ID': - case 'Title': - // console.log(rowData); - let bvlLink = this.props.baseURL + '/issue_tracker/issue/' + rowData["Issue ID"]; - result = ({rowData[column]}); - break; - case 'Status': - switch (cell) { - case 'closed': - case 'resolved': - result = ({cell}); - break; - case 'new': - case 'assigned': - case 'acknowledged': - case 'feedback': - result = ({cell}); - break; - default: - result = ({cell}); - break; - } - break; - case 'Description': - let a = cell.replace(/"/g,'"'); - result = ({a}); - break; - default: - result = ({cell}); - break; - } - return result; + // background color + let bgGreen = {textAlign: 'center', backgroundColor: '#EDF7E5'}; + let bgRed = {textAlign: 'center', backgroundColor: '#F7D6E0'}; + let bgYellow = {textAlign: 'center', backgroundColor: '#FFF696'}; + + switch (column) { + case 'Issue ID': + case 'Title': + // console.log(rowData); + let bvlLink = this.props.baseURL + + '/issue_tracker/issue/' + + rowData['Issue ID']; + result = ({rowData[column]}); + break; + case 'Status': + switch (cell) { + case 'closed': + case 'resolved': + result = ({cell}); + break; + case 'new': + case 'assigned': + case 'acknowledged': + case 'feedback': + result = ({cell}); + break; + default: + result = ({cell}); + break; + } + break; + case 'Description': + let a = cell.replace(/"/g, '"'); + result = ({a}); + break; + default: + result = ({cell}); + break; + } + return result; } /** @@ -126,71 +126,71 @@ class IssuesViewer extends Component { render() { // Waiting for async data to load. if (!this.state.isLoaded) { - return ; + return ; } // The fields configured for display/hide. let fields = [ - { - label: "Issue ID", - show: false, - type: 'text', - }, - { - label: "Title", - show: true, - type: 'text', - filter: { - name: 'Title', - type: 'text', - } + { + label: 'Issue ID', + show: false, + type: 'text', + }, + { + label: 'Title', + show: true, + type: 'text', + filter: { + name: 'Title', + type: 'text', }, - { - label: "Status", - show: true, - type: 'text', - filter: { - name: 'Status', - type: 'text', - } + }, + { + label: 'Status', + show: true, + type: 'text', + filter: { + name: 'Status', + type: 'text', }, - { - label: "Date Created", - show: true, - type: 'text', - filter: { - name: 'Date Created', - type: 'text', - } + }, + { + label: 'Date Created', + show: true, + type: 'text', + filter: { + name: 'Date Created', + type: 'text', }, - { - label: "Description", - show: true, - type: 'text', - filter: { - name: 'Description', - type: 'text', - } + }, + { + label: 'Description', + show: true, + type: 'text', + filter: { + name: 'Description', + type: 'text', }, + }, ]; return (
{this.state.data == null ? (
- Error: no issues found. -
) + Error: no issues found. +
) : <> -
+
- Note: Only the last 10.000 issues will be - displayed. If more are required, please check the issue tracker. + Note: Only the last 10.000 issues will be + displayed. If more are required, please check the issue tracker.
- } @@ -205,4 +205,4 @@ IssuesViewer.propTypes = { moduleURL: PropTypes.string.isRequired, }; -export default IssuesViewer; \ No newline at end of file +export default IssuesViewer; diff --git a/modules/redcap/jsx/tabs/notificationViewer.js b/modules/redcap/jsx/tabs/notificationViewer.js index 35329043c09..42c5fd7d9c4 100644 --- a/modules/redcap/jsx/tabs/notificationViewer.js +++ b/modules/redcap/jsx/tabs/notificationViewer.js @@ -5,192 +5,190 @@ import FilterableDataTable from 'jsx/FilterableDataTable'; class NotificationViewer extends Component { - - /** - * Constructor of component - * - * @param {object} props - the component properties. - */ - constructor(props) { - super(props); - - this.state = { - baseURL: props.baseURL, - data: {}, - error: false, - isLoaded: false, - }; - - this.fetchData = this.fetchData.bind(this); - this.formatColumn = this.formatColumn.bind(this); - } - - /** - * Called by React when the component has been rendered on the page. - */ - componentDidMount() { - this.fetchData(); - } - - /** - * Retrieve data for the form and save it in state. - */ - fetchData() { - fetch(`${this.state.baseURL}/?format=json`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - }).then((resp) => { - if (!resp.ok) { - this.setState({error: true}); - console.error(resp.status + ': ' + resp.statusText); - return; - } - - resp.json() - .then((data) => { - this.setState({ - data, - isLoaded: true, - }); - }); - - }).catch((error) => { - this.setState({error: true}); - console.error(error); + /** + * Constructor of component + * + * @param {object} props - the component properties. + */ + constructor(props) { + super(props); + + this.state = { + baseURL: props.baseURL, + data: {}, + error: false, + isLoaded: false, + }; + + this.fetchData = this.fetchData.bind(this); + this.formatColumn = this.formatColumn.bind(this); + } + + /** + * Called by React when the component has been rendered on the page. + */ + componentDidMount() { + this.fetchData(); + } + + /** + * Retrieve data for the form and save it in state. + */ + fetchData() { + fetch(`${this.state.baseURL}/?format=json`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + }).then((resp) => { + if (!resp.ok) { + this.setState({error: true}); + console.error(resp.status + ': ' + resp.statusText); + return; + } + + resp.json() + .then((data) => { + this.setState({ + data, + isLoaded: true, + }); }); + }).catch((error) => { + this.setState({error: true}); + console.error(error); + }); + } + + /** + * Modify behaviour of specified column cells in the Data Table component + * + * @param {string} column - column name + * @param {string} cell - cell content + * @param {object} row - row content indexed by column + * + * @return {*} a formatted table cell for a given column + */ + formatColumn(column, cell, row) { + let result = ({cell}); + + // background color + let bgGreen = {textAlign: 'center', backgroundColor: '#EDF7E5'}; + let bgRed = {textAlign: 'center', backgroundColor: '#F7D6E0'}; + + switch (column) { + case 'Complete': + // yes/no answers + result = (cell === '2') + ? (✅) + : (❌); + break; + default: + result = ({cell}); + break; } - - /** - * Modify behaviour of specified column cells in the Data Table component - * - * @param {string} column - column name - * @param {string} cell - cell content - * @param {object} row - row content indexed by column - * - * @return {*} a formatted table cell for a given column - */ - formatColumn(column, cell, row) { - let result = ({cell}); - - // background color - let bgGreen = {textAlign: "center", backgroundColor: "#EDF7E5"}; - let bgRed = {textAlign: "center", backgroundColor: "#F7D6E0"}; - - switch (column) { - case 'Complete': - // yes/no answers - result = (cell === '2') - ? (✅) - : (❌); - break; - default: - result = ({cell}); - break; - } - return result; + return result; + } + + /** + * @return {JSX} the incomplete form to render. + */ + render() { + // Waiting for async data to load. + if (!this.state.isLoaded) { + return ; } + // The fields configured for display/hide. /** - * @return {JSX} the incomplete form to render. + * Fields. */ - render() { - // Waiting for async data to load. - if (!this.state.isLoaded) { - return ; - } - - // The fields configured for display/hide. - /** - * Fields. - */ - const fields = [ - { - label: 'Notification ID', - show: false, - type: 'text', - }, - { - label: 'Complete', - show: true, - type: 'text', - }, - { - label: 'Record ID', - show: true, - type: 'text', - filter: { - name: 'Record ID', - type: 'text', - } - }, - { - label: 'Event Name', - show: true, - type: 'text', - filter: { - name: 'Event Name', - type: 'text', - } - }, - { - label: 'Instrument', - show: true, - type: 'text', - filter: { - name: 'Instrument', - type: 'text', - } - }, - { - label: 'REDCap URL', - show: true, - type: 'text', - }, - { - label: 'Project ID', - show: true, - type: 'text', - filter: { - name: 'Project ID', - type: 'text', - } - }, - { - label: 'Received', - show: true, - type: 'date', - }, - { - label: 'Handled', - show: true, - type: 'date', - }, - ]; - - return ( - <> -
-
- Note: Only the last 10.000 notifications will be - displayed. If more are required, please contact the administrator. -
- - - ); - } + const fields = [ + { + label: 'Notification ID', + show: false, + type: 'text', + }, + { + label: 'Complete', + show: true, + type: 'text', + }, + { + label: 'Record ID', + show: true, + type: 'text', + filter: { + name: 'Record ID', + type: 'text', + }, + }, + { + label: 'Event Name', + show: true, + type: 'text', + filter: { + name: 'Event Name', + type: 'text', + }, + }, + { + label: 'Instrument', + show: true, + type: 'text', + filter: { + name: 'Instrument', + type: 'text', + }, + }, + { + label: 'REDCap URL', + show: true, + type: 'text', + }, + { + label: 'Project ID', + show: true, + type: 'text', + filter: { + name: 'Project ID', + type: 'text', + }, + }, + { + label: 'Received', + show: true, + type: 'date', + }, + { + label: 'Handled', + show: true, + type: 'date', + }, + ]; + + return ( + <> +
+
+ Note: Only the last 10.000 notifications will be + displayed. If more are required, please contact the administrator. +
+ + + ); + } } NotificationViewer.propTypes = { - data: PropTypes.object, - baseURL: PropTypes.string.isRequired, + data: PropTypes.object, + baseURL: PropTypes.string.isRequired, }; -export default NotificationViewer; \ No newline at end of file +export default NotificationViewer; From fff6a8ea8c3ace8c71fa2515d69c95092eb53d43 Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 16 Dec 2025 16:48:33 -0500 Subject: [PATCH 33/33] lint typo --- modules/redcap/jsx/tabs/dictionaryViewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/redcap/jsx/tabs/dictionaryViewer.js b/modules/redcap/jsx/tabs/dictionaryViewer.js index 05282666d89..6192c61619a 100644 --- a/modules/redcap/jsx/tabs/dictionaryViewer.js +++ b/modules/redcap/jsx/tabs/dictionaryViewer.js @@ -1,4 +1,4 @@ -import React, {Component} from 'reac'; +import React, {Component} from 'react'; import PropTypes from 'prop-types'; import Loader from 'jsx/Loader'; import FilterableDataTable from 'jsx/FilterableDataTable';