From 1a6dc393d25694f8b7e5ee6e3f3b92cd08c7908e Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 22 May 2025 20:15:13 -0400 Subject: [PATCH 01/30] redcap config - load all config file --- .../php/config/redcapconfigparser.class.inc | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/modules/redcap/php/config/redcapconfigparser.class.inc b/modules/redcap/php/config/redcapconfigparser.class.inc index ec0bfa1f0bd..cd91e9ebe35 100644 --- a/modules/redcap/php/config/redcapconfigparser.class.inc +++ b/modules/redcap/php/config/redcapconfigparser.class.inc @@ -448,4 +448,83 @@ class RedcapConfigParser . " (redcap-project-id: {$this->_redcap_project_id}): $message" ); } + + /** + * Get the full REDCap configuration as described in 'config.xml' file. + * Returns a struct ordered by REDCap instance and project. + * Expected structure is: + * [REDCap instance URL => [REDCap Project ID => REDCapConfig object]] + * else null if no configurations are found. + * + * @param \LORIS\LorisInstance $loris the loris instance + * + * @throws \LorisException + * + * @return array[] a REDCap configuration with the form + * [REDCap instance URL => [REDCap Project ID => REDCapConfig object]] + * else null + */ + public static function getConfiguration( + \LORIS\LorisInstance $loris + ): array { + // final REDCap configuration struct + $redcapConfig = []; + + // list of REDCap instances and projects + $rawRedcapConfig = $loris->getConfiguration()->getSetting('redcap'); + if ($rawRedcapConfig === null || empty($rawRedcapConfig)) { + throw new \LorisException("No REDCap configuration."); + } + + // instances, must at least have one + $redcapInstances = $rawRedcapConfig['instance'] ?? null; + if ($redcapInstances === null || empty($redcapInstances)) { + throw new \LorisException("No REDCap instance defined in configuration file."); + } + + // only one instance, wrap it in an array to be able to iterate + if (!array_key_exists(0, $redcapInstances)) { + $redcapInstances = [$redcapInstances]; + } + + // going throught REDCap defined instances + foreach ($redcapInstances as $instanceStruct) { + // instance URL + $redcapInstanceURL = $instanceStruct['redcap-url']; + + // project of this instance + $redcapProjects = $instanceStruct['project'] ?? null; + if ($redcapProjects === null || empty($redcapProjects)) { + // no project for that instance, next REDCap instance + continue; + } + + // only one project, wrap it in an array to be able to iterate + if (!array_key_exists(0, $redcapProjects)) { + $redcapProjects = [$redcapProjects]; + } + + // add REDCap configuration + foreach ($redcapProjects as $redcapProject) { + // project ID + $redcapProjectID = $redcapProject['redcap-project-id']; + + // parser object + $configParser = new RedcapConfigParser( + $loris, + $redcapInstanceURL, + $redcapProjectID + ); + + // add the parsed REDCap configuration to the final struct + $redcapConfig[$redcapInstanceURL] = [ + ...$redcapConfig[$redcapInstanceURL] ?? [], + $redcapProjectID => $configParser->parse() + ]; + } + } + + // return struct + return empty($redcapConfig) ? null : $redcapConfig; + } } From df7d06201637fd6c4a48775ca5b9c9d1abf11a21 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 22 May 2025 20:16:21 -0400 Subject: [PATCH 02/30] redcap tool - bulk importer - draft --- tools/redcap/redcap_bulk_importer.php | 546 ++++++++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 tools/redcap/redcap_bulk_importer.php diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php new file mode 100644 index 00000000000..3a32b00538c --- /dev/null +++ b/tools/redcap/redcap_bulk_importer.php @@ -0,0 +1,546 @@ +#!/usr/bin/env php + + * @license Loris license + * @link https://www.github.com/aces/Loris/ + */ + +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); + +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); +} + +use \GuzzleHttp\Client; +use Psr\Http\Message\ResponseInterface; + +use LORIS\StudyEntities\Candidate\CandID; + +use LORIS\redcap\config\RedcapConfigLorisId; +use LORIS\redcap\config\RedcapConfigParser; +use LORIS\redcap\client\RedcapHttpClient; + + +// -------------------------------------- + +/** + * All REDCap connections (http client) for each REDCap instance/project. + * @var mixed + */ +$redcapConnections = []; + +// /** +// * All REDCap unique event names (event + arm) for each REDCap instance/project. +// * @var mixed +// */ +// $redcapEventMap = []; + +// /** +// * All REDCap instruments for each REDCap instance/project. +// * @var mixed +// */ +// $redcapInstrumentMap = []; + +/** + * All REDCap instrument-event map for each REDCap instance/project. + * @var mixed + */ +$redcapInstrumentEventMap = []; + + +// -------------------------------------- +// TODO: arg parse. +$verbose = true; +$forceUpdate = false; // force even complete instrument? +$redcapUsername = "bulk_import"; // the name that will be used in the redcap notification + +// -------------------------------------- +// arg display +$verboseMsg = $verbose ? 'enabled' : 'disabled'; +fprintf(STDOUT, "[args:verbose] {$verboseMsg}\n"); +$forceUpdateMsg = $forceUpdate ? 'enabled' : 'disabled'; +fprintf(STDOUT, "[args:force_update_instruments] {$forceUpdateMsg}\n"); + + +// -------------------------------------- +// LORIS URL +try { + $lorisURL = $lorisInstance->getConfiguration()->getSetting('baseURL'); +} catch (\Throwable $th) { + $lorisURL = \NDB_Factory::singleton()->settings()->getBaseURL(); +} +if (empty($lorisURL)) { + fprintf(STDERR, "[loris:config:base_url] no LORIS base URL.\n"); + exit(1); +} +fprintf(STDOUT, "[loris:url] LORIS instance used: {$lorisURL}\n"); + +// init LORIS client +$lorisClient = new Client( + ['base_uri' => "$lorisURL/redcap/notifications"] +); + +// Load all LORIS importable instruments (across all REDCap instances) +$redcapAllowedInstruments = $config->getSetting('redcap_importable_instrument'); +if (empty($redcapAllowedInstruments)) { + fprintf(STDERR, "[redcap:configuration] no instrument authorized.\n"); + exit(3); +} + +// Get LORIS data to import +fprintf(STDOUT, "[loris:data] getting data to import...\n"); +$lorisDataToImport = getLORISInstrumentToImport( + $lorisInstance->getDatabaseConnection(), + $redcapAllowedInstruments, + $forceUpdate +); +if (empty($lorisDataToImport)) { + fprintf(STDERR, "[loris:data] no data to import.\n"); + exit(0); +} + +$cLorisData = count($lorisDataToImport); +$forceMsg = $forceUpdate ? " (already 'Complete' instruments too)" : " ('In Progress' instruments)"; +fprintf(STDOUT, "[loris:data] records to import from REDCap{$forceMsg}: {$cLorisData}\n"); + +// Load REDCap configuration +fprintf(STDOUT, "[redcap:configuration] getting REDCap configurations...\n"); +try { + $redcapConfiguration = RedcapConfigParser::getConfiguration($lorisInstance); +} catch (\LorisException $th) { + fprintf(STDERR, "[redcap:configuration] {$th->getMessage()}.\n"); + exit(3); +} +// +if ($redcapConfiguration === null) { + fprintf(STDERR, "[redcap:configuration] no REDCap configuration in 'config.xml'.\n"); + exit(3); +} + +// error_log(print_r(array_keys($redcapConfiguration['https://redcap.iths.org/']), true)); +// error_log(print_r($redcapConfiguration['https://redcap.iths.org/'][98505], true)); + +# Load REDCap connections +fprintf(STDOUT, "[redcap:connections] building REDCap connections...\n"); +initREDCapConnections( + $redcapConfiguration, + $redcapConnections, + false +); + +// Test REDCap connections +fprintf(STDOUT, "[redcap:connections] testing REDCap connections...\n"); +testREDCapConnections($redcapConnections); + +// // Load REDCap unique event names +// fprintf(STDOUT, "[redcap:connections] loading REDCap events...\n"); +// initREDCapEventMap( +// $redcapConnections, +// $redcapEventMap +// ); + +// // Load REDCap instruments, only consider importable instruments +// fprintf(STDOUT, "[redcap:connections] loading REDCap instruments...\n"); +// initREDCapInstrumentMap( +// $redcapConnections, +// $redcapAllowedInstruments, +// $redcapInstrumentMap +// ); + +// Load REDCap instrument-event, only consider importable instruments +fprintf(STDOUT, "[redcap:connections] loading REDCap instrument-event mapping...\n"); +initREDCapInstrumentEventMap( + $redcapConnections, + $redcapAllowedInstruments, + $redcapInstrumentEventMap +); + +// error_log(print_r($redcapInstrumentEventMap['https://redcap.iths.org/'][98505], true)); + + +// iterating over all records +fprintf(STDERR, "[loris:redcap_endpoint] triggering notifications and import...\n"); +foreach($lorisDataToImport as $index => $instrumentToQuery) { + // LORIS param + $pscid = $instrumentToQuery['pscid']; + $candid = $instrumentToQuery['candid']; + $visitLabel = $instrumentToQuery['visitLabel']; + $instrumentName = $instrumentToQuery['instrument']; + $commentID = $instrumentToQuery['commentID']; + + // candidate + $candidate = \Candidate::singleton(new CandID($candid)); + + // log + fprintf(STDOUT, "[{$index}][pscid:{$pscid}|candid:{$candid}][visit:{$visitLabel}][instrument:{$instrumentName}] \n"); + + // select REDCap instances and projects that have this instrument + // [redcap instance URL => [redcap project ID => RedcapInstrumentEventMap object]] + // ideally, there should only be one instance and one project selected + // target event-instrument mapping from REDCap based on instrument name + $redcapTargetedEventInstrument = getTargetedEventInstrument( + $redcapInstrumentEventMap, + $instrumentName + ); + + // count number of selected event-instrument mappings across instances + $nbProjects = array_reduce( + $redcapTargetedEventInstrument, + fn($c, $i) => $c + count($i), + 0 + ); + if ($nbProjects === 0) { + fprintf(STDERR, " - no REDCap instances/projects for that instrument.\n"); + continue; + } + if ($nbProjects > 1) { + // TODO: what if an instrument name is in different instances/projects? + // TODO: logic needs to be implemented more largely in the rest of the codebase. + fprintf(STDERR, " - multiple REDCap instances/projects with that instrument.\n"); + continue; + } + + // explicit values + $redcapInstanceURL = array_keys($redcapTargetedEventInstrument)[0]; + $redcapProjectID = array_keys($redcapTargetedEventInstrument[$redcapInstanceURL])[0]; + $redcapEventInstrument = $redcapTargetedEventInstrument[$redcapInstanceURL][$redcapProjectID]; + + fprintf(STDOUT, " - instrument found in '{$redcapInstanceURL}', pid:{$redcapProjectID}\n"); + + // get this instance/project configuration + // TODO: which candidate ID? PSCID/CANDID? + // TODO: which participant ID? RecordID/surveyID? + // TODO: which visit? visit mapping in config. + $redcapConfig = $redcapConfiguration[$redcapInstanceURL][$redcapProjectID]; + + // candidate ID to use + // TODO: maybe some change here depending on the REDCap way of naming + $redcapRecordID = match ($redcapConfig->candidate_id) { + RedcapConfigLorisId::PscId => $candidate->getPSCID(), + RedcapConfigLorisId::CandId => $candidate->getCandID(), + }; + + // send POST request to LORIS mimicing REDCap notification + $response = sendNotification( + $lorisClient, + "{$redcapInstanceURL}/api/", + $redcapProjectID, + $instrumentName, + $redcapRecordID, + $redcapEventInstrument->unique_event_name, + $redcapUsername + ); + + // + $responseStatusMsg = $response->getStatusCode() != 200 ? "failed" : "ok"; + fprintf(STDERR, " - {$responseStatusMsg}\n"); +} + +// -------------------------------------- + +/** + * Initialize REDCap connections based on the given configuration. + * + * @param array $redcapConfig + * @param array $redcapConnections + * @param bool $verbose + * + * @return void + */ +function initREDCapConnections( + array $redcapConfig, + array &$redcapConnections, + bool $verbose = false +): void { + foreach ($redcapConfig as $redcapURL => $redcapInstance) { + foreach ($redcapInstance as $redcapProjectID => $redcapConfig) { + $cleanURL = rtrim($redcapURL, '/'); + $redcapConnections[$redcapURL] = [ + ...$redcapConnections[$redcapURL] ?? [], + $redcapProjectID => new RedcapHttpClient( + "{$cleanURL}/api/", + $redcapConfig->redcap_api_token, + $verbose + ) + ]; + } + } +} + +/** + * Test all REDCap connections based on a given connection structure. + * + * @param array $redcapConnections + * + * @return void + */ +function testREDCapConnections( + array $redcapConnections +): void { + foreach ($redcapConnections as $redcapURL => $redcapInstance) { + foreach ($redcapInstance as $redcapProjectID => $redcapClient) { + $redcapClient->checkConnection(); + } + } +} + +// /** +// * Initialize REDCap events based on the given connection. +// * +// * @param array $redcapConnections +// * @param array $redcapEventMap +// * +// * @return void +// */ +// function initREDCapEventMap( +// array $redcapConnections, +// array &$redcapEventMap +// ): void { +// foreach ($redcapConnections as $redcapURL => $redcapInstance) { +// foreach ($redcapInstance as $redcapProjectID => $redcapClient) { +// $redcapEventMap[$redcapURL] = [ +// ...$redcapEventMap[$redcapURL] ?? [], +// $redcapProjectID => $redcapClient->getEvents() +// ]; +// } +// } +// } + +// /** +// * Initialize REDCap instruments based on the given connection. +// * +// * @param array $redcapConnections +// * @param array $redcapAllowedInstruments +// * @param array $redcapInstrumentMap +// * +// * @return void +// */ +// function initREDCapInstrumentMap( +// array $redcapConnections, +// array $redcapAllowedInstruments, +// array &$redcapInstrumentMap +// ): void { +// foreach ($redcapConnections as $redcapURL => $redcapInstance) { +// foreach ($redcapInstance as $redcapProjectID => $redcapClient) { +// // all instruments from a client +// $instruments = $redcapClient->getInstruments(); + +// // filter allowed instruments +// $allowedInstruments = array_filter( +// $instruments, +// fn($i) => in_array( +// $i->name, +// $redcapAllowedInstruments, +// true +// ) +// ); + +// // +// if (!empty($allowedInstruments)) { +// $redcapInstrumentMap[$redcapURL] = [ +// ...$redcapInstrumentMap[$redcapURL] ?? [], +// $redcapProjectID => $allowedInstruments +// ]; +// } +// } +// } +// } + +/** + * Initialize REDCap instrument-event based on the given connection. + * + * @param array $redcapConnections + * @param array $redcapAllowedInstruments + * @param array $redcapInstrumentMap + * + * @return void + */ +function initREDCapInstrumentEventMap( + array $redcapConnections, + array $redcapAllowedInstruments, + array &$redcapInstrumentEventMap +): void { + foreach ($redcapConnections as $redcapURL => $redcapInstance) { + foreach ($redcapInstance as $redcapProjectID => $redcapClient) { + // instrument-event map from a client + $instrumentEventMap = $redcapClient->getInstrumentEventMapping(); + + // filter allowed instruments + $allowedInstruments = array_filter( + $instrumentEventMap, + fn($i) => in_array( + $i->form_name, + $redcapAllowedInstruments, + true + ) + ); + + // + if (!empty($allowedInstruments)) { + $redcapInstrumentEventMap[$redcapURL] = [ + ...$redcapInstrumentEventMap[$redcapURL] ?? [], + $redcapProjectID => $allowedInstruments + ]; + } + } + } +} + +/** + * Get the list of all instrument records to import from REDCap. + * + * @param Database $db + * @param array $allowedRedcapInstruments + * @param bool $forceUpdate + * + * @return array + */ +function getLORISInstrumentToImport( + \Database $db, + array $allowedRedcapInstruments, + bool $forceUpdate = false +): array { + // importable redcap instruments + $selectedInstruments = "('" + . implode("','", $allowedRedcapInstruments) + . "')"; + + // if forced, then we do not filter out administration/data entry already + // set up, they will be overridden + $forceUpdateCondition = ""; + if (!$forceUpdate) { + $forceUpdateCondition = " + AND (f.Data_entry = 'In Progress' OR f.Data_entry IS NULL) + AND f.Administration IS NULL + "; + } + + // + return $db->pselect( + "SELECT c.PSCID as pscid, + c.CandID as candid, + s.Visit_label as visitLabel, + f.test_name as instrument, + f.CommentID as commentID, + f.Data_entry as dataEntry, + f.Administration as administration + FROM flag f + JOIN session s ON (s.ID = f.sessionID) + JOIN candidate c ON (c.candid = s.candid) + WHERE s.Active = 'Y' + AND c.Active = 'Y' + AND f.CommentID NOT LIKE 'DDE%' + {$forceUpdateCondition} + AND f.test_name IN {$selectedInstruments} + ", + [] + ); +} + +/** + * Send a REDCap notification to LORIS. + * + * @param GuzzleHttp\Client $lorisClient LORIS client + * @param string $redcapAPIURL the REDCap API URL to use + * @param int $redcapProjectID the REDCap project ID + * @param string $redcapInstrumentName the REDCap instrument name + * @param string $redcapRecordID the REDCap Record ID + * @param string $redcapUniqueEventName the REDCap unique event name + * @param string $redcapUsername the REDCap user + * + * @return ResponseInterface + */ +function sendNotification( + Client $lorisClient, + string $redcapAPIURL, + int $redcapProjectID, + string $redcapInstrumentName, + string $redcapRecordID, + string $redcapUniqueEventName, + string $redcapUsername +): ResponseInterface { + // From REDCap doc - Data Entry Trigger composition. + // - project_id - The unique ID number of the REDCap project (i.e. the 'pid' value found in the URL when accessing the project in REDCap). + // - username - The username of the REDCap user that is triggering the Data Entry Trigger. Note: If it is triggered by a survey page (as opposed to a data entry form), then the username that will be reported will be '[survey respondent]'. + // - instrument - The unique name of the current data collection instrument (all your project's unique instrument names can be found in column B in the data dictionary). + // - record - The name of the record being created or modified, which is the record's value for the project's first field. + // - redcap_event_name - The unique event name of the event for which the record was modified (for longitudinal projects only). + // - redcap_data_access_group - The unique group name of the Data Access Group to which the record belongs (if the record belongs to a group). + // - [instrument]_complete - The status of the record for this particular data collection instrument, in which the value will be 0, 1, or 2. For data entry forms, 0=Incomplete, 1=Unverified, 2=Complete. For surveys, 0=partial survey response and 2=completed survey response. This parameter's name will be the variable name of this particular instrument's status field, which is the name of the instrument + '_complete'. + // - redcap_repeat_instance - The repeat instance number of the current instance of a repeating event OR repeating instrument. Note: This parameter is only sent in the request if the project contains repeating events/instruments *and* is currently saving a repeating event/instrument. + // - redcap_repeat_instrument - The unique instrument name of the current repeating instrument being saved. Note: This parameter is only sent in the request if the project contains repeating instruments *and* is currently saving a repeating instrument. Also, this parameter will not be sent for repeating events (as opposed to repeating instruments). + // - redcap_url - The base web address to REDCap (URL of REDCap's home page). e.g., https://redcap.iths.org/ + // - project_url - The base web address to the current REDCap project (URL of its Project Home page). e.g., https://redcap.iths.org/redcap_v15.1.2/index.php?pid=XXXX + + $data = [ + "redcap_url" => $redcapAPIURL, + "project_id" => $redcapProjectID, + "project_url" => "", + "instrument" => $redcapInstrumentName, + "record" => $redcapRecordID, + "redcap_event_name" => $redcapUniqueEventName, + "username" => $redcapUsername, + "{$redcapInstrumentName}_complete" => "2", + ]; + + // send + return $lorisClient->request( + 'POST', + '', + [ + 'form_params' => $data, + 'debug' => false + ] + ); +} + +/** + * Get a targeted event-instrument mapping. + * This returns a structure + * [redcap instance URL => [redcap project ID => RedcapInstrumentEventMap object]] + * when the given instrument is found in any redcap event-instrument mapping. + * + * @param mixed $redcapInstrumentEventMap + * @param mixed $instrumentName + * + * @return array[] + */ +function getTargetedEventInstrument( + $redcapInstrumentEventMap, + $instrumentName +): array { + $selectedRedcapProject = []; + foreach ($redcapInstrumentEventMap as $redcapURL => $redcapInstance) { + foreach ($redcapInstance as $redcapProjectID => $redcapInstruments) { + // search instrument + $foundInstrumentEventMap = array_filter( + $redcapInstruments, + fn($i) => $i->form_name === $instrumentName + ); + + // get the precise mapping "RedcapInstrumentEventMap" object + if (!empty($foundInstrumentEventMap)) { + $selectedRedcapProject[$redcapURL] = [ + ...$selectedRedcapProject[$redcapURL] ?? [], + $redcapProjectID => array_values($foundInstrumentEventMap)[0] + ]; + } + } + } + return $selectedRedcapProject; +}; \ No newline at end of file From a7842ea14b0d88fd49bdee230c86b10b85ea3c5c Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 22 May 2025 20:51:52 -0400 Subject: [PATCH 03/30] lint --- modules/redcap/php/config/redcapconfigparser.class.inc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/redcap/php/config/redcapconfigparser.class.inc b/modules/redcap/php/config/redcapconfigparser.class.inc index cd91e9ebe35..d49725ffee5 100644 --- a/modules/redcap/php/config/redcapconfigparser.class.inc +++ b/modules/redcap/php/config/redcapconfigparser.class.inc @@ -479,7 +479,9 @@ class RedcapConfigParser // instances, must at least have one $redcapInstances = $rawRedcapConfig['instance'] ?? null; if ($redcapInstances === null || empty($redcapInstances)) { - throw new \LorisException("No REDCap instance defined in configuration file."); + throw new \LorisException( + "No REDCap instance defined in configuration file." + ); } // only one instance, wrap it in an array to be able to iterate From f28abd7f51252fa73795f73b4bea18130662f925 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 22 May 2025 21:07:44 -0400 Subject: [PATCH 04/30] lint --- tools/redcap/redcap_bulk_importer.php | 120 ++++++++++++++------------ 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index 3a32b00538c..066327d473e 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -1,5 +1,6 @@ #!/usr/bin/env php getMessage()}.\n"); exit(3); } -// +// if ($redcapConfiguration === null) { fprintf(STDERR, "[redcap:configuration] no REDCap configuration in 'config.xml'.\n"); exit(3); } -// error_log(print_r(array_keys($redcapConfiguration['https://redcap.iths.org/']), true)); -// error_log(print_r($redcapConfiguration['https://redcap.iths.org/'][98505], true)); - -# Load REDCap connections +// Load REDCap connections fprintf(STDOUT, "[redcap:connections] building REDCap connections...\n"); initREDCapConnections( $redcapConfiguration, @@ -170,12 +183,15 @@ $redcapInstrumentEventMap ); -// error_log(print_r($redcapInstrumentEventMap['https://redcap.iths.org/'][98505], true)); +error_log(print_r( + $redcapInstrumentEventMap['https://redcap.iths.org/'][98505], + true +)); // iterating over all records fprintf(STDERR, "[loris:redcap_endpoint] triggering notifications and import...\n"); -foreach($lorisDataToImport as $index => $instrumentToQuery) { +foreach ($lorisDataToImport as $index => $instrumentToQuery) { // LORIS param $pscid = $instrumentToQuery['pscid']; $candid = $instrumentToQuery['candid']; @@ -190,14 +206,16 @@ fprintf(STDOUT, "[{$index}][pscid:{$pscid}|candid:{$candid}][visit:{$visitLabel}][instrument:{$instrumentName}] \n"); // select REDCap instances and projects that have this instrument - // [redcap instance URL => [redcap project ID => RedcapInstrumentEventMap object]] + // [redcap instance URL => + // [redcap project ID => RedcapInstrumentEventMap object] + // ] // ideally, there should only be one instance and one project selected // target event-instrument mapping from REDCap based on instrument name $redcapTargetedEventInstrument = getTargetedEventInstrument( $redcapInstrumentEventMap, $instrumentName ); - + // count number of selected event-instrument mappings across instances $nbProjects = array_reduce( $redcapTargetedEventInstrument, @@ -210,18 +228,26 @@ } if ($nbProjects > 1) { // TODO: what if an instrument name is in different instances/projects? - // TODO: logic needs to be implemented more largely in the rest of the codebase. - fprintf(STDERR, " - multiple REDCap instances/projects with that instrument.\n"); + // TODO: logic needs to be implemented more largely in the rest of the + // TODO: codebase. + fprintf( + STDERR, + " - multiple REDCap instances/projects with that instrument.\n" + ); continue; } // explicit values $redcapInstanceURL = array_keys($redcapTargetedEventInstrument)[0]; - $redcapProjectID = array_keys($redcapTargetedEventInstrument[$redcapInstanceURL])[0]; - $redcapEventInstrument = $redcapTargetedEventInstrument[$redcapInstanceURL][$redcapProjectID]; - - fprintf(STDOUT, " - instrument found in '{$redcapInstanceURL}', pid:{$redcapProjectID}\n"); - + $instanceData = $redcapTargetedEventInstrument[$redcapInstanceURL]; + $redcapProjectID = array_keys($instanceData)[0]; + $redcapEventInstrument = $instanceData[$redcapProjectID]; + + fprintf( + STDOUT, + " - instrument found in '{$redcapInstanceURL}', pid:{$redcapProjectID}\n" + ); + // get this instance/project configuration // TODO: which candidate ID? PSCID/CANDID? // TODO: which participant ID? RecordID/surveyID? @@ -245,7 +271,7 @@ $redcapEventInstrument->unique_event_name, $redcapUsername ); - + // $responseStatusMsg = $response->getStatusCode() != 200 ? "failed" : "ok"; fprintf(STDERR, " - {$responseStatusMsg}\n"); @@ -256,9 +282,9 @@ /** * Initialize REDCap connections based on the given configuration. * - * @param array $redcapConfig - * @param array $redcapConnections - * @param bool $verbose + * @param array $redcapConfig REDCap configuration structure + * @param array $redcapConnections REDCap connection structure to fill + * @param bool $verbose Verbose mode * * @return void */ @@ -285,7 +311,7 @@ function initREDCapConnections( /** * Test all REDCap connections based on a given connection structure. * - * @param array $redcapConnections + * @param array $redcapConnections REDCap connection structure * * @return void */ @@ -364,9 +390,9 @@ function testREDCapConnections( /** * Initialize REDCap instrument-event based on the given connection. * - * @param array $redcapConnections - * @param array $redcapAllowedInstruments - * @param array $redcapInstrumentMap + * @param array $redcapConnections REDCap connections structure + * @param array $redcapAllowedInstruments Allowed instruments + * @param array $redcapInstrumentEventMap REDCAp event-instrument mapping * * @return void */ @@ -404,9 +430,9 @@ function initREDCapInstrumentEventMap( /** * Get the list of all instrument records to import from REDCap. * - * @param Database $db - * @param array $allowedRedcapInstruments - * @param bool $forceUpdate + * @param Database $db database object + * @param array $allowedRedcapInstruments authorized redcap instruments + * @param bool $forceUpdate do force update? * * @return array */ @@ -454,7 +480,7 @@ function getLORISInstrumentToImport( /** * Send a REDCap notification to LORIS. - * + * * @param GuzzleHttp\Client $lorisClient LORIS client * @param string $redcapAPIURL the REDCap API URL to use * @param int $redcapProjectID the REDCap project ID @@ -474,19 +500,7 @@ function sendNotification( string $redcapUniqueEventName, string $redcapUsername ): ResponseInterface { - // From REDCap doc - Data Entry Trigger composition. - // - project_id - The unique ID number of the REDCap project (i.e. the 'pid' value found in the URL when accessing the project in REDCap). - // - username - The username of the REDCap user that is triggering the Data Entry Trigger. Note: If it is triggered by a survey page (as opposed to a data entry form), then the username that will be reported will be '[survey respondent]'. - // - instrument - The unique name of the current data collection instrument (all your project's unique instrument names can be found in column B in the data dictionary). - // - record - The name of the record being created or modified, which is the record's value for the project's first field. - // - redcap_event_name - The unique event name of the event for which the record was modified (for longitudinal projects only). - // - redcap_data_access_group - The unique group name of the Data Access Group to which the record belongs (if the record belongs to a group). - // - [instrument]_complete - The status of the record for this particular data collection instrument, in which the value will be 0, 1, or 2. For data entry forms, 0=Incomplete, 1=Unverified, 2=Complete. For surveys, 0=partial survey response and 2=completed survey response. This parameter's name will be the variable name of this particular instrument's status field, which is the name of the instrument + '_complete'. - // - redcap_repeat_instance - The repeat instance number of the current instance of a repeating event OR repeating instrument. Note: This parameter is only sent in the request if the project contains repeating events/instruments *and* is currently saving a repeating event/instrument. - // - redcap_repeat_instrument - The unique instrument name of the current repeating instrument being saved. Note: This parameter is only sent in the request if the project contains repeating instruments *and* is currently saving a repeating instrument. Also, this parameter will not be sent for repeating events (as opposed to repeating instruments). - // - redcap_url - The base web address to REDCap (URL of REDCap's home page). e.g., https://redcap.iths.org/ - // - project_url - The base web address to the current REDCap project (URL of its Project Home page). e.g., https://redcap.iths.org/redcap_v15.1.2/index.php?pid=XXXX - + // data to send $data = [ "redcap_url" => $redcapAPIURL, "project_id" => $redcapProjectID, @@ -511,18 +525,18 @@ function sendNotification( /** * Get a targeted event-instrument mapping. - * This returns a structure + * This returns a structure * [redcap instance URL => [redcap project ID => RedcapInstrumentEventMap object]] * when the given instrument is found in any redcap event-instrument mapping. - * - * @param mixed $redcapInstrumentEventMap - * @param mixed $instrumentName - * + * + * @param array $redcapInstrumentEventMap the event-instrument mapping + * @param string $instrumentName the searched instrument name + * * @return array[] */ function getTargetedEventInstrument( - $redcapInstrumentEventMap, - $instrumentName + array $redcapInstrumentEventMap, + string $instrumentName ): array { $selectedRedcapProject = []; foreach ($redcapInstrumentEventMap as $redcapURL => $redcapInstance) { From 0be46d1d99764555c371eda25ed17cf418241563 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 22 May 2025 21:17:33 -0400 Subject: [PATCH 05/30] lint --- tools/redcap/redcap_bulk_importer.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index 066327d473e..9fd8c2799b6 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -144,7 +144,10 @@ } // if ($redcapConfiguration === null) { - fprintf(STDERR, "[redcap:configuration] no REDCap configuration in 'config.xml'.\n"); + fprintf( + STDERR, + "[redcap:configuration] no REDCap configuration in 'config.xml'.\n" + ); exit(3); } @@ -183,10 +186,12 @@ $redcapInstrumentEventMap ); -error_log(print_r( - $redcapInstrumentEventMap['https://redcap.iths.org/'][98505], - true -)); +error_log( + print_r( + $redcapInstrumentEventMap['https://redcap.iths.org/'][98505], + true + ) +); // iterating over all records @@ -203,7 +208,11 @@ $candidate = \Candidate::singleton(new CandID($candid)); // log - fprintf(STDOUT, "[{$index}][pscid:{$pscid}|candid:{$candid}][visit:{$visitLabel}][instrument:{$instrumentName}] \n"); + $log = "[{$index}]"; + $log .= "[pscid:{$pscid}|candid:{$candid}]"; + $log .= "[visit:{$visitLabel}]"; + $log .= "[instrument:{$instrumentName}]"; + fprintf(STDOUT, "{$log} \n"); // select REDCap instances and projects that have this instrument // [redcap instance URL => From c49657acc5cf2e5392a28bedbf977cd6abdc6c3b Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 23 May 2025 17:19:40 -0400 Subject: [PATCH 06/30] generic include path --- tools/redcap/redcap_bulk_importer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index 9fd8c2799b6..02e57ef3b08 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -17,7 +17,7 @@ ini_set('display_startup_errors', 1); error_reporting(E_ALL); -require_once __DIR__ . "/../../../tools/generic_includes.php"; +require_once __DIR__ . "/../../tools/generic_includes.php"; // load redcap module try { From 7016b291b0b567ad3ec04ec3b74159fc97728a30 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 23 May 2025 17:20:14 -0400 Subject: [PATCH 07/30] arg parse --- tools/redcap/redcap_bulk_importer.php | 106 +++++++++++++++++++------- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index 02e57ef3b08..eb3de3d7bb0 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -69,41 +69,36 @@ // -------------------------------------- -// TODO: arg parse. - -// verbose -$verbose = true; - -// force even complete instrument? -$forceUpdate = false; +// ARGS PARSE +$options = getopt( + "", + [ + "loris-url:", + "redcap-username:", + "force-update", + "verbose", + ] +); +$opts = checkOptions($options); -// the name that will be used in the redcap notification -$redcapUsername = "bulk_import"; +// args mapping +$lorisURL = $opts['lorisURL']; +$redcapUsername = $opts['redcapUsername']; +$forceUpdate = $opts['forceUpdate']; +$verbose = $opts['verbose']; -// -------------------------------------- // arg display +fprintf(STDOUT, "[args:loris_url] {$lorisURL}\n"); $verboseMsg = $verbose ? 'enabled' : 'disabled'; fprintf(STDOUT, "[args:verbose] {$verboseMsg}\n"); $forceUpdateMsg = $forceUpdate ? 'enabled' : 'disabled'; -fprintf(STDOUT, "[args:force_update_instruments] {$forceUpdateMsg}\n"); - +fprintf(STDOUT, "[args:force_update] {$forceUpdateMsg}\n"); +fprintf(STDOUT, "[args:redcap_username] {$redcapUsername}\n"); // -------------------------------------- -// LORIS URL -try { - $lorisURL = $lorisInstance->getConfiguration()->getSetting('baseURL'); -} catch (\Throwable $th) { - $lorisURL = \NDB_Factory::singleton()->settings()->getBaseURL(); -} -if (empty($lorisURL)) { - fprintf(STDERR, "[loris:config:base_url] no LORIS base URL.\n"); - exit(1); -} -fprintf(STDOUT, "[loris:url] LORIS instance used: {$lorisURL}\n"); - // init LORIS client $lorisClient = new Client( - ['base_uri' => "$lorisURL/redcap/notifications"] + ['base_uri' => "{$lorisURL}/redcap/notifications"] ); // Load all LORIS importable instruments (across all REDCap instances) @@ -566,4 +561,63 @@ function getTargetedEventInstrument( } } return $selectedRedcapProject; -}; \ No newline at end of file +}; + +// -------------------------------------- +// Utility + +/** + * Check arguments passed to this script. + * + * @param array $options the arguments + * + * @return array clean and valid version. + */ +function checkOptions(array $options) { + // loris URL, mandatory + if (!isset($options['loris-url'])) { + error_log("[error] Required parameter: 'loris-path'."); + showHelp(); + exit(1); + } + $lorisURL = rtrim($options['loris-url'], '/'); + + // redcap-username + $redcapUsername = $options['redcap-username'] ?? 'bulk_import'; + + // force-update + $forceUpdate = isset($options['force-update']); + + // verbose + $verbose = isset($options['verbose']); + + // clean + return [ + 'lorisURL' => $lorisURL, + 'redcapUsername' => $redcapUsername, + 'forceUpdate' => $forceUpdate, + 'verbose' => $verbose, + ]; +} + +/** + * Displays help for this script. + * + * @return void + */ +function showHelp() : void { + fprintf( + STDERR, + "Usage:\n" + . " php redcap_bulk_importer.php \n" + . " {--loris-url=LORISURL}\n" + . " [--redcap-username=USER]\n" + . " [--force-update]\n" + . " [--verbose]\n\n" + . "Notes:\n" + . " - required, '--loris-url=LORISURL' the loris URL\n" + . " - optional, '--redcap-username=USER' the text that will be in all redcap notification as the REDCap username. (default: 'bulk_import')\n" + . " - optional, '--force-update' needs to force update on complete instrument? (default: false)\n" + . " - optional, '--verbose' verbose mode? (default: false)\n\n" + ); +} \ No newline at end of file From b152c5a73d0150612995302fa5d12464d983925a Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 23 May 2025 17:20:37 -0400 Subject: [PATCH 08/30] candid str --- tools/redcap/redcap_bulk_importer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index eb3de3d7bb0..c0dc895190f 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -200,7 +200,7 @@ $commentID = $instrumentToQuery['commentID']; // candidate - $candidate = \Candidate::singleton(new CandID($candid)); + $candidate = \Candidate::singleton(new CandID("{$candid}")); // log $log = "[{$index}]"; From bfaf8cb8b640de43d60b57addad4b95a8f948457 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 23 May 2025 17:21:02 -0400 Subject: [PATCH 09/30] db select query iterator to array --- tools/redcap/redcap_bulk_importer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index c0dc895190f..e619848f655 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -461,7 +461,7 @@ function getLORISInstrumentToImport( } // - return $db->pselect( + return iterator_to_array($db->pselect( "SELECT c.PSCID as pscid, c.CandID as candid, s.Visit_label as visitLabel, @@ -479,7 +479,7 @@ function getLORISInstrumentToImport( AND f.test_name IN {$selectedInstruments} ", [] - ); + )); } /** From 046d2d7838cea49a875a0f754eee1214fb940183 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 23 May 2025 17:27:43 -0400 Subject: [PATCH 10/30] array to query object --- tools/redcap/redcap_bulk_importer.php | 29 ++++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index e619848f655..b135004fb0f 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -30,6 +30,7 @@ use \GuzzleHttp\Client; use Psr\Http\Message\ResponseInterface; +use LORIS\Database\Query; use LORIS\StudyEntities\Candidate\CandID; use LORIS\redcap\config\RedcapConfigLorisId; @@ -115,13 +116,14 @@ $redcapAllowedInstruments, $forceUpdate ); -if (empty($lorisDataToImport)) { + +$cLorisData = $lorisDataToImport->count(); +if ($cLorisData === 0) { fprintf(STDERR, "[loris:data] no data to import.\n"); exit(0); } -$cLorisData = count($lorisDataToImport); -$forceMsg = $forceUpdate +$forceMsg = $forceUpdate ? "(already 'Complete' instruments too)" : "('In Progress' instruments)"; fprintf( @@ -181,13 +183,12 @@ $redcapInstrumentEventMap ); -error_log( - print_r( - $redcapInstrumentEventMap['https://redcap.iths.org/'][98505], - true - ) -); - +// error_log( +// print_r( +// $redcapInstrumentEventMap['https://redcap.iths.org/'][98505], +// true +// ) +// ); // iterating over all records fprintf(STDERR, "[loris:redcap_endpoint] triggering notifications and import...\n"); @@ -438,13 +439,13 @@ function initREDCapInstrumentEventMap( * @param array $allowedRedcapInstruments authorized redcap instruments * @param bool $forceUpdate do force update? * - * @return array + * @return Query */ function getLORISInstrumentToImport( \Database $db, array $allowedRedcapInstruments, bool $forceUpdate = false -): array { +): Query { // importable redcap instruments $selectedInstruments = "('" . implode("','", $allowedRedcapInstruments) @@ -461,7 +462,7 @@ function getLORISInstrumentToImport( } // - return iterator_to_array($db->pselect( + return $db->pselect( "SELECT c.PSCID as pscid, c.CandID as candid, s.Visit_label as visitLabel, @@ -479,7 +480,7 @@ function getLORISInstrumentToImport( AND f.test_name IN {$selectedInstruments} ", [] - )); + ); } /** From 9cc984e10c37353097d203deed38651d534105e4 Mon Sep 17 00:00:00 2001 From: regisoc Date: Fri, 23 May 2025 18:01:41 -0400 Subject: [PATCH 11/30] lint --- tools/redcap/redcap_bulk_importer.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tools/redcap/redcap_bulk_importer.php b/tools/redcap/redcap_bulk_importer.php index b135004fb0f..28928200fe6 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/tools/redcap/redcap_bulk_importer.php @@ -80,6 +80,8 @@ "verbose", ] ); + +// $opts = checkOptions($options); // args mapping @@ -574,7 +576,8 @@ function getTargetedEventInstrument( * * @return array clean and valid version. */ -function checkOptions(array $options) { +function checkOptions(array $options): array +{ // loris URL, mandatory if (!isset($options['loris-url'])) { error_log("[error] Required parameter: 'loris-path'."); @@ -606,7 +609,8 @@ function checkOptions(array $options) { * * @return void */ -function showHelp() : void { +function showHelp() : void +{ fprintf( STDERR, "Usage:\n" @@ -616,9 +620,13 @@ function showHelp() : void { . " [--force-update]\n" . " [--verbose]\n\n" . "Notes:\n" - . " - required, '--loris-url=LORISURL' the loris URL\n" - . " - optional, '--redcap-username=USER' the text that will be in all redcap notification as the REDCap username. (default: 'bulk_import')\n" - . " - optional, '--force-update' needs to force update on complete instrument? (default: false)\n" + . " - required, '--loris-url=LORISURL' " + . "the loris URL\n" + . " - optional, '--redcap-username=USER' " + . "the text that will be in all redcap notification as the REDCap " + . "username. (default: 'bulk_import')\n" + . " - optional, '--force-update' needs to force update on " + . "complete instrument? (default: false)\n" . " - optional, '--verbose' verbose mode? (default: false)\n\n" ); } \ No newline at end of file From 1fc591b76ddadbfa13395be72e32da3a497e57cb Mon Sep 17 00:00:00 2001 From: regisoc Date: Tue, 27 May 2025 11:26:58 -0400 Subject: [PATCH 12/30] mv tools to redcap module --- .../redcap => modules/redcap/tools}/redcap_bulk_importer.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename {tools/redcap => modules/redcap/tools}/redcap_bulk_importer.php (99%) diff --git a/tools/redcap/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php similarity index 99% rename from tools/redcap/redcap_bulk_importer.php rename to modules/redcap/tools/redcap_bulk_importer.php index 28928200fe6..e4c56ccd14a 100644 --- a/tools/redcap/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -17,7 +17,7 @@ ini_set('display_startup_errors', 1); error_reporting(E_ALL); -require_once __DIR__ . "/../../tools/generic_includes.php"; +require_once __DIR__ . "/../../../tools/generic_includes.php"; // load redcap module try { @@ -203,7 +203,8 @@ $commentID = $instrumentToQuery['commentID']; // candidate - $candidate = \Candidate::singleton(new CandID("{$candid}")); + $candidObj = new CandID("{$candid}"); + $candidate = \Candidate::singleton($candidObj); // log $log = "[{$index}]"; From 6e3130145213793e990b330400a2deff7cdcfa00 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 28 May 2025 16:30:43 -0400 Subject: [PATCH 13/30] query - loris instrument/commentIDs to be imported --- modules/redcap/php/queries.class.inc | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/modules/redcap/php/queries.class.inc b/modules/redcap/php/queries.class.inc index 29c4008a56f..05a9ff1da32 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\Database\Query; use \LORIS\StudyEntities\Candidate\CandID; /** @@ -283,4 +284,62 @@ class Queries { $this->_db->run("UNLOCK TABLES"); } + + /** + * Get the list of all instrument records to import from REDCap. + * + * @param array $allowedRedcapInstruments authorized redcap instruments + * @param bool $forceUpdate do force update? + * + * @return Query + */ + public function getLORISInstrumentToImport( + array $allowedRedcapInstruments, + bool $forceUpdate = false + ): Query { + // extract first importable instrument name + $firstInstrumentName = $allowedRedcapInstruments[0]; + $allOtherInstrumentNames = array_slice($allowedRedcapInstruments, 1); + + // build query for exact instrument name match + $selectedInstruments = "AND ("; + $selectedInstruments .= " BINARY f.test_name = '{$firstInstrumentName}'"; + foreach ($allOtherInstrumentNames as $instrumentName) { + $selectedInstruments .= " OR BINARY f.test_name = '{$instrumentName}'"; + } + $selectedInstruments .= ")"; + + // if forced, then we do not filter out administration/data entry already + // set up, they will be overridden + $forceUpdateCondition = ""; + if (!$forceUpdate) { + $forceUpdateCondition = " + AND (f.Data_entry = 'In Progress' OR f.Data_entry IS NULL) + AND f.Administration IS NULL + "; + } + + $query = "SELECT c.PSCID as pscid, + c.CandID as candid, + s.Visit_label as visitLabel, + f.test_name as instrument, + f.CommentID as commentID, + f.Data_entry as dataEntry, + f.Administration as administration + FROM flag f + JOIN session s ON (s.ID = f.sessionID) + JOIN candidate c ON (c.candid = s.candid) + WHERE s.Active = 'Y' + AND c.Active = 'Y' + AND f.CommentID NOT LIKE 'DDE%' + {$forceUpdateCondition} + {$selectedInstruments} + "; + + // + return $this->_db->pselect( + $query, + [] + ); + } } From a6b334af6f2d7935dd1393f6797493fda0a4db88 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 28 May 2025 16:36:52 -0400 Subject: [PATCH 14/30] queries --- modules/redcap/tools/redcap_bulk_importer.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index e4c56ccd14a..2e34a3256a4 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -36,6 +36,7 @@ use LORIS\redcap\config\RedcapConfigLorisId; use LORIS\redcap\config\RedcapConfigParser; use LORIS\redcap\client\RedcapHttpClient; +use LORIS\redcap\Queries; // -------------------------------------- @@ -68,6 +69,13 @@ */ $redcapInstrumentEventMap = []; +/** + * REDCap related queries. + * + * @var Queries + */ +$queries = new Queries($lorisInstance); + // -------------------------------------- // ARGS PARSE @@ -99,6 +107,8 @@ fprintf(STDOUT, "[args:redcap_username] {$redcapUsername}\n"); // -------------------------------------- +// Main process + // init LORIS client $lorisClient = new Client( ['base_uri' => "{$lorisURL}/redcap/notifications"] @@ -113,8 +123,7 @@ // Get LORIS data to import fprintf(STDOUT, "[loris:data] getting data to import...\n"); -$lorisDataToImport = getLORISInstrumentToImport( - $lorisInstance->getDatabaseConnection(), +$lorisDataToImport = $queries->getLORISInstrumentToImport( $redcapAllowedInstruments, $forceUpdate ); From 0db999418c2a0401633cfd3e4cc51b99568d4cee Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 28 May 2025 16:38:37 -0400 Subject: [PATCH 15/30] loris instrument to import to queries --- modules/redcap/tools/redcap_bulk_importer.php | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 2e34a3256a4..d496090d61f 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -444,57 +444,6 @@ function initREDCapInstrumentEventMap( } } -/** - * Get the list of all instrument records to import from REDCap. - * - * @param Database $db database object - * @param array $allowedRedcapInstruments authorized redcap instruments - * @param bool $forceUpdate do force update? - * - * @return Query - */ -function getLORISInstrumentToImport( - \Database $db, - array $allowedRedcapInstruments, - bool $forceUpdate = false -): Query { - // importable redcap instruments - $selectedInstruments = "('" - . implode("','", $allowedRedcapInstruments) - . "')"; - - // if forced, then we do not filter out administration/data entry already - // set up, they will be overridden - $forceUpdateCondition = ""; - if (!$forceUpdate) { - $forceUpdateCondition = " - AND (f.Data_entry = 'In Progress' OR f.Data_entry IS NULL) - AND f.Administration IS NULL - "; - } - - // - return $db->pselect( - "SELECT c.PSCID as pscid, - c.CandID as candid, - s.Visit_label as visitLabel, - f.test_name as instrument, - f.CommentID as commentID, - f.Data_entry as dataEntry, - f.Administration as administration - FROM flag f - JOIN session s ON (s.ID = f.sessionID) - JOIN candidate c ON (c.candid = s.candid) - WHERE s.Active = 'Y' - AND c.Active = 'Y' - AND f.CommentID NOT LIKE 'DDE%' - {$forceUpdateCondition} - AND f.test_name IN {$selectedInstruments} - ", - [] - ); -} - /** * Send a REDCap notification to LORIS. * From f03fb51cedb33fc3884614123d6762edef37da6a Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 28 May 2025 16:38:58 -0400 Subject: [PATCH 16/30] trigger notifications fn --- modules/redcap/tools/redcap_bulk_importer.php | 198 ++++++++++-------- 1 file changed, 113 insertions(+), 85 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index d496090d61f..010134dbd0d 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -203,98 +203,126 @@ // iterating over all records fprintf(STDERR, "[loris:redcap_endpoint] triggering notifications and import...\n"); -foreach ($lorisDataToImport as $index => $instrumentToQuery) { - // LORIS param - $pscid = $instrumentToQuery['pscid']; - $candid = $instrumentToQuery['candid']; - $visitLabel = $instrumentToQuery['visitLabel']; - $instrumentName = $instrumentToQuery['instrument']; - $commentID = $instrumentToQuery['commentID']; - - // candidate - $candidObj = new CandID("{$candid}"); - $candidate = \Candidate::singleton($candidObj); - - // log - $log = "[{$index}]"; - $log .= "[pscid:{$pscid}|candid:{$candid}]"; - $log .= "[visit:{$visitLabel}]"; - $log .= "[instrument:{$instrumentName}]"; - fprintf(STDOUT, "{$log} \n"); - - // select REDCap instances and projects that have this instrument - // [redcap instance URL => - // [redcap project ID => RedcapInstrumentEventMap object] - // ] - // ideally, there should only be one instance and one project selected - // target event-instrument mapping from REDCap based on instrument name - $redcapTargetedEventInstrument = getTargetedEventInstrument( - $redcapInstrumentEventMap, - $instrumentName - ); +triggerNotifications( + $lorisInstance, + $lorisClient, + $lorisDataToImport, + $redcapInstrumentEventMap, + $redcapConfiguration, + $redcapUsername +); - // count number of selected event-instrument mappings across instances - $nbProjects = array_reduce( - $redcapTargetedEventInstrument, - fn($c, $i) => $c + count($i), - 0 - ); - if ($nbProjects === 0) { - fprintf(STDERR, " - no REDCap instances/projects for that instrument.\n"); - continue; - } - if ($nbProjects > 1) { - // TODO: what if an instrument name is in different instances/projects? - // TODO: logic needs to be implemented more largely in the rest of the - // TODO: codebase. - fprintf( - STDERR, - " - multiple REDCap instances/projects with that instrument.\n" +// -------------------------------------- + +/** + * Trigger the selected set of notification to import data in LORIS. + * + * @param GuzzleHttp\Client $lorisClient the loris client + * @param LORIS\Database\Query $lorisDataToImport the list of data to import + * @param array $redcapInstrumentEventMap the instrument-event map + * @param array $redcapConfiguration the REDCap conf + * @param string $redcapUsername the text to pass + * + * @return void + */ +function triggerNotifications( + \LORIS\LorisInstance $loris, // override + GuzzleHttp\Client $lorisClient, + LORIS\Database\Query $lorisDataToImport, + array $redcapInstrumentEventMap, + array $redcapConfiguration, + string $redcapUsername +): void { + foreach ($lorisDataToImport as $index => $instrumentToQuery) { + // LORIS param + $pscid = $instrumentToQuery['pscid']; + $candid = $instrumentToQuery['candid']; + $visitLabel = $instrumentToQuery['visitLabel']; + $instrumentName = $instrumentToQuery['instrument']; + $commentID = $instrumentToQuery['commentID']; + + // candidate + $candidate = \Candidate::singleton(new CandID("{$candid}")); + + // log + $log = "[{$index}]"; + $log .= "[pscid:{$pscid}|candid:{$candid}]"; + $log .= "[visit:{$visitLabel}]"; + $log .= "[instrument:{$instrumentName}]"; + fprintf(STDOUT, "{$log} \n"); + + // select REDCap instances and projects that have this instrument + // [redcap instance URL => + // [redcap project ID => RedcapInstrumentEventMap object] + // ] + // ideally, there should only be one instance and one project selected + // target event-instrument mapping from REDCap based on instrument name + $redcapTargetedEventInstrument = getTargetedEventInstrument( + $redcapInstrumentEventMap, + $instrumentName ); - continue; - } - // explicit values - $redcapInstanceURL = array_keys($redcapTargetedEventInstrument)[0]; - $instanceData = $redcapTargetedEventInstrument[$redcapInstanceURL]; - $redcapProjectID = array_keys($instanceData)[0]; - $redcapEventInstrument = $instanceData[$redcapProjectID]; + // count number of selected event-instrument mappings across instances + $nbProjects = array_reduce( + $redcapTargetedEventInstrument, + fn($c, $i) => $c + count($i), + 0 + ); + if ($nbProjects === 0) { + fprintf(STDERR, " - no REDCap instances/projects for that instrument.\n"); + continue; + } + if ($nbProjects > 1) { + // TODO: what if an instrument name is in different instances/projects? + // TODO: logic needs to be implemented more largely in the rest of the + // TODO: codebase. + fprintf( + STDERR, + " - multiple REDCap instances/projects with that instrument.\n" + ); + continue; + } - fprintf( - STDOUT, - " - instrument found in '{$redcapInstanceURL}', pid:{$redcapProjectID}\n" - ); + // explicit values + $redcapInstanceURL = array_keys($redcapTargetedEventInstrument)[0]; + $instanceData = $redcapTargetedEventInstrument[$redcapInstanceURL]; + $redcapProjectID = array_keys($instanceData)[0]; + $redcapEventInstrument = $instanceData[$redcapProjectID]; - // get this instance/project configuration - // TODO: which candidate ID? PSCID/CANDID? - // TODO: which participant ID? RecordID/surveyID? - // TODO: which visit? visit mapping in config. - $redcapConfig = $redcapConfiguration[$redcapInstanceURL][$redcapProjectID]; - - // candidate ID to use - // TODO: maybe some change here depending on the REDCap way of naming - $redcapRecordID = match ($redcapConfig->candidate_id) { - RedcapConfigLorisId::PscId => $candidate->getPSCID(), - RedcapConfigLorisId::CandId => $candidate->getCandID(), - }; - - // send POST request to LORIS mimicing REDCap notification - $response = sendNotification( - $lorisClient, - "{$redcapInstanceURL}/api/", - $redcapProjectID, - $instrumentName, - $redcapRecordID, - $redcapEventInstrument->unique_event_name, - $redcapUsername - ); + fprintf( + STDOUT, + " - instrument found in '{$redcapInstanceURL}', pid:{$redcapProjectID}\n" + ); - // - $responseStatusMsg = $response->getStatusCode() != 200 ? "failed" : "ok"; - fprintf(STDERR, " - {$responseStatusMsg}\n"); -} + // get this instance/project configuration + // TODO: which candidate ID? PSCID/CANDID? + // TODO: which participant ID? RecordID/surveyID? + // TODO: which visit? visit mapping in config. + $redcapConfig = $redcapConfiguration[$redcapInstanceURL][$redcapProjectID]; + + // candidate ID to use + // TODO: maybe some change here depending on the REDCap way of naming + $redcapRecordID = match ($redcapConfig->candidate_id) { + RedcapConfigLorisId::PscId => $candidate->getPSCID(), + RedcapConfigLorisId::CandId => $candidate->getCandID(), + }; + + // send POST request to LORIS mimicing REDCap notification + $response = sendNotification( + $lorisClient, + "{$redcapInstanceURL}/api/", + $redcapProjectID, + $instrumentName, + $redcapRecordID, + $redcapEventInstrument->unique_event_name, + $redcapUsername + ); -// -------------------------------------- + // + $responseStatusMsg = $response->getStatusCode() != 200 ? "failed" : "ok"; + fprintf(STDERR, " - {$responseStatusMsg}\n"); + } +} /** * Initialize REDCap connections based on the given configuration. From 1b662f795342f76f3d39eb4249fc7b4989d9a282 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 28 May 2025 16:46:04 -0400 Subject: [PATCH 17/30] rm override mention --- modules/redcap/tools/redcap_bulk_importer.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 010134dbd0d..ad856ac5cba 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -204,7 +204,6 @@ // iterating over all records fprintf(STDERR, "[loris:redcap_endpoint] triggering notifications and import...\n"); triggerNotifications( - $lorisInstance, $lorisClient, $lorisDataToImport, $redcapInstrumentEventMap, @@ -226,7 +225,6 @@ * @return void */ function triggerNotifications( - \LORIS\LorisInstance $loris, // override GuzzleHttp\Client $lorisClient, LORIS\Database\Query $lorisDataToImport, array $redcapInstrumentEventMap, From e95a7e8d2c8ada88406c1b29cdedba14bf5740fd Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 29 May 2025 08:23:59 -0400 Subject: [PATCH 18/30] url path --- modules/redcap/tools/redcap_bulk_importer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index ad856ac5cba..00916caa5ab 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -507,7 +507,7 @@ function sendNotification( // send return $lorisClient->request( 'POST', - '', + '/redcap/notifications', [ 'form_params' => $data, 'debug' => false From 96d052b0fe2d50e0ab0997a9336828c76a4573a7 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 29 May 2025 14:44:57 -0400 Subject: [PATCH 19/30] guzzle exception mgmt --- modules/redcap/tools/redcap_bulk_importer.php | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 00916caa5ab..7fd0618937a 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -306,18 +306,27 @@ function triggerNotifications( }; // send POST request to LORIS mimicing REDCap notification - $response = sendNotification( - $lorisClient, - "{$redcapInstanceURL}/api/", - $redcapProjectID, - $instrumentName, - $redcapRecordID, - $redcapEventInstrument->unique_event_name, - $redcapUsername - ); + try { + $response = sendNotification( + $lorisClient, + $redcapInstanceURL, + $redcapProjectID, + $instrumentName, + $redcapRecordID, + $redcapEventInstrument->unique_event_name, + $redcapUsername + ); + $responseStatusMsg = $response->getStatusCode() != 200 ? "failed" : "ok"; + } catch (GuzzleHttp\Exception\ServerException $ex) { // 500-level + $msg = $ex->getResponse()->getBody()->getContents(); + error_log($msg); + $responseStatusMsg = "[5xx] failed"; + } catch (GuzzleHttp\Exception\ClientException $ex) { // 400-level + $msg = $ex->getResponse()->getBody()->getContents(); + error_log($msg); + $responseStatusMsg = "[4xx] failed"; + } - // - $responseStatusMsg = $response->getStatusCode() != 200 ? "failed" : "ok"; fprintf(STDERR, " - {$responseStatusMsg}\n"); } } From 549024bd38cd78eb209f9f3a49babe131e674662 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 14:01:55 -0400 Subject: [PATCH 20/30] lint --- modules/redcap/tools/redcap_bulk_importer.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 7fd0618937a..40acb34286b 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -267,7 +267,10 @@ function triggerNotifications( 0 ); if ($nbProjects === 0) { - fprintf(STDERR, " - no REDCap instances/projects for that instrument.\n"); + fprintf( + STDERR, + " - no REDCap instances/projects for that instrument.\n" + ); continue; } if ($nbProjects > 1) { @@ -316,6 +319,8 @@ function triggerNotifications( $redcapEventInstrument->unique_event_name, $redcapUsername ); + + // get status message $responseStatusMsg = $response->getStatusCode() != 200 ? "failed" : "ok"; } catch (GuzzleHttp\Exception\ServerException $ex) { // 500-level $msg = $ex->getResponse()->getBody()->getContents(); From 44a3dda5aa44845af8529beec6d69568321f927c Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 14:48:48 -0400 Subject: [PATCH 21/30] queries method/parameters renaming --- modules/redcap/php/queries.class.inc | 15 +++++++++------ modules/redcap/tools/redcap_bulk_importer.php | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/modules/redcap/php/queries.class.inc b/modules/redcap/php/queries.class.inc index 05a9ff1da32..b50b701ea2d 100644 --- a/modules/redcap/php/queries.class.inc +++ b/modules/redcap/php/queries.class.inc @@ -288,18 +288,21 @@ class Queries /** * Get the list of all instrument records to import from REDCap. * - * @param array $allowedRedcapInstruments authorized redcap instruments - * @param bool $forceUpdate do force update? + * @param array $allowedRedcapInstrumentNames authorized redcap instruments + * @param bool $forceUpdate do force update? * * @return Query */ - public function getLORISInstrumentToImport( - array $allowedRedcapInstruments, + public function getLORISInstrumentsToImport( + array $allowedRedcapInstrumentNames, bool $forceUpdate = false ): Query { // extract first importable instrument name - $firstInstrumentName = $allowedRedcapInstruments[0]; - $allOtherInstrumentNames = array_slice($allowedRedcapInstruments, 1); + $firstInstrumentName = $allowedRedcapInstrumentNames[0]; + $allOtherInstrumentNames = array_slice( + $allowedRedcapInstrumentNames, + 1 + ); // build query for exact instrument name match $selectedInstruments = "AND ("; diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 40acb34286b..f990ca0f331 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -123,7 +123,7 @@ // Get LORIS data to import fprintf(STDOUT, "[loris:data] getting data to import...\n"); -$lorisDataToImport = $queries->getLORISInstrumentToImport( +$lorisDataToImport = $queries->getLORISInstrumentsToImport( $redcapAllowedInstruments, $forceUpdate ); From 4f6d65c76e66dbfefba5f2b8a7eea2bb3c589321 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 14:55:35 -0400 Subject: [PATCH 22/30] renaming --- modules/redcap/tools/redcap_bulk_importer.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index f990ca0f331..058f26c3e1d 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -115,8 +115,8 @@ ); // Load all LORIS importable instruments (across all REDCap instances) -$redcapAllowedInstruments = $config->getSetting('redcap_importable_instrument'); -if (empty($redcapAllowedInstruments)) { +$redcapAllowedInstrumentNames = $config->getSetting('redcap_importable_instrument'); +if (empty($redcapAllowedInstrumentNames)) { fprintf(STDERR, "[redcap:configuration] no instrument authorized.\n"); exit(3); } @@ -124,7 +124,7 @@ // Get LORIS data to import fprintf(STDOUT, "[loris:data] getting data to import...\n"); $lorisDataToImport = $queries->getLORISInstrumentsToImport( - $redcapAllowedInstruments, + $redcapAllowedInstrumentNames, $forceUpdate ); @@ -182,7 +182,7 @@ // fprintf(STDOUT, "[redcap:connections] loading REDCap instruments...\n"); // initREDCapInstrumentMap( // $redcapConnections, -// $redcapAllowedInstruments, +// $redcapAllowedInstrumentNames, // $redcapInstrumentMap // ); @@ -190,7 +190,7 @@ fprintf(STDOUT, "[redcap:connections] loading REDCap instrument-event mapping...\n"); initREDCapInstrumentEventMap( $redcapConnections, - $redcapAllowedInstruments, + $redcapAllowedInstrumentNames, $redcapInstrumentEventMap ); @@ -408,14 +408,14 @@ function testREDCapConnections( // * Initialize REDCap instruments based on the given connection. // * // * @param array $redcapConnections -// * @param array $redcapAllowedInstruments +// * @param array $redcapAllowedInstrumentNames // * @param array $redcapInstrumentMap // * // * @return void // */ // function initREDCapInstrumentMap( // array $redcapConnections, -// array $redcapAllowedInstruments, +// array $redcapAllowedInstrumentNames, // array &$redcapInstrumentMap // ): void { // foreach ($redcapConnections as $redcapURL => $redcapInstance) { @@ -428,7 +428,7 @@ function testREDCapConnections( // $instruments, // fn($i) => in_array( // $i->name, -// $redcapAllowedInstruments, +// $redcapAllowedInstrumentNames, // true // ) // ); @@ -448,14 +448,14 @@ function testREDCapConnections( * Initialize REDCap instrument-event based on the given connection. * * @param array $redcapConnections REDCap connections structure - * @param array $redcapAllowedInstruments Allowed instruments - * @param array $redcapInstrumentEventMap REDCAp event-instrument mapping + * @param array $redcapAllowedInstrumentNames Allowed instruments + * @param array> $redcapInstrumentEventMap REDCap event-instrument mapping * * @return void */ function initREDCapInstrumentEventMap( array $redcapConnections, - array $redcapAllowedInstruments, + array $redcapAllowedInstrumentNames, array &$redcapInstrumentEventMap ): void { foreach ($redcapConnections as $redcapURL => $redcapInstance) { @@ -468,7 +468,7 @@ function initREDCapInstrumentEventMap( $instrumentEventMap, fn($i) => in_array( $i->form_name, - $redcapAllowedInstruments, + $redcapAllowedInstrumentNames, true ) ); From 6a6f50046d8562827f169a8412ce50a80a759957 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 15:27:01 -0400 Subject: [PATCH 23/30] importer - docstring types --- modules/redcap/tools/redcap_bulk_importer.php | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 058f26c3e1d..23d0a067760 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -30,9 +30,9 @@ use \GuzzleHttp\Client; use Psr\Http\Message\ResponseInterface; -use LORIS\Database\Query; use LORIS\StudyEntities\Candidate\CandID; +use LORIS\redcap\client\models\mappings\RedcapInstrumentEventMap; use LORIS\redcap\config\RedcapConfigLorisId; use LORIS\redcap\config\RedcapConfigParser; use LORIS\redcap\client\RedcapHttpClient; @@ -44,7 +44,7 @@ /** * All REDCap connections (http client) for each REDCap instance/project. * - * @var mixed + * @var array */ $redcapConnections = []; @@ -65,7 +65,7 @@ /** * All REDCap instrument-event map for each REDCap instance/project. * - * @var mixed + * @var array> */ $redcapInstrumentEventMap = []; @@ -216,11 +216,11 @@ /** * Trigger the selected set of notification to import data in LORIS. * - * @param GuzzleHttp\Client $lorisClient the loris client - * @param LORIS\Database\Query $lorisDataToImport the list of data to import - * @param array $redcapInstrumentEventMap the instrument-event map - * @param array $redcapConfiguration the REDCap conf - * @param string $redcapUsername the text to pass + * @param GuzzleHttp\Client $lorisClient the loris client + * @param LORIS\Database\Query $lorisDataToImport the list of data to import + * @param array> $redcapInstrumentEventMap the instrument-event map + * @param array[] $redcapConfiguration the REDCap conf + * @param string $redcapUsername the text to pass * * @return void */ @@ -339,9 +339,9 @@ function triggerNotifications( /** * Initialize REDCap connections based on the given configuration. * - * @param array $redcapConfig REDCap configuration structure - * @param array $redcapConnections REDCap connection structure to fill - * @param bool $verbose Verbose mode + * @param array[] $redcapConfig REDCap configuration structure + * @param array $redcapConnections REDCap connection structure to fill + * @param bool $verbose Verbose mode * * @return void */ @@ -368,7 +368,7 @@ function initREDCapConnections( /** * Test all REDCap connections based on a given connection structure. * - * @param array $redcapConnections REDCap connection structure + * @param array $redcapConnections REDCap connection structure * * @return void */ @@ -447,9 +447,9 @@ function testREDCapConnections( /** * Initialize REDCap instrument-event based on the given connection. * - * @param array $redcapConnections REDCap connections structure - * @param array $redcapAllowedInstrumentNames Allowed instruments - * @param array> $redcapInstrumentEventMap REDCap event-instrument mapping + * @param array $redcapConnections REDCap connections structure + * @param array $redcapAllowedInstrumentNames Allowed instruments + * @param array> $redcapInstrumentEventMap REDCap event-instrument mapping * * @return void */ @@ -538,7 +538,7 @@ function sendNotification( * @param array $redcapInstrumentEventMap the event-instrument mapping * @param string $instrumentName the searched instrument name * - * @return array[] + * @return array> an array */ function getTargetedEventInstrument( array $redcapInstrumentEventMap, @@ -573,7 +573,12 @@ function getTargetedEventInstrument( * * @param array $options the arguments * - * @return array clean and valid version. + * @return array{ + * lorisURL: string, + * redcapUsername: string, + * forceUpdate: bool, + * verbose: bool + * } clean and valid options. */ function checkOptions(array $options): array { From e1fcb3a23b9dacf10147ee80e3d214fff493f024 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 15:32:13 -0400 Subject: [PATCH 24/30] lint - docstring test --- modules/redcap/tools/redcap_bulk_importer.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 23d0a067760..b444ad0f68a 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -447,9 +447,12 @@ function testREDCapConnections( /** * Initialize REDCap instrument-event based on the given connection. * - * @param array $redcapConnections REDCap connections structure - * @param array $redcapAllowedInstrumentNames Allowed instruments - * @param array> $redcapInstrumentEventMap REDCap event-instrument mapping + * @param array + * $redcapConnections REDCap connections structure + * @param array + * $redcapAllowedInstrumentNames Allowed instruments + * @param array> + * $redcapInstrumentEventMap REDCap event-instrument mapping * * @return void */ From 72f4ca1d7b2535f7ec2075fb471cb7b1b437a298 Mon Sep 17 00:00:00 2001 From: regisoc Date: Wed, 9 Jul 2025 16:21:50 -0400 Subject: [PATCH 25/30] lint - docstring test --- modules/redcap/tools/redcap_bulk_importer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index b444ad0f68a..eecc7426413 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -447,11 +447,11 @@ function testREDCapConnections( /** * Initialize REDCap instrument-event based on the given connection. * - * @param array + * @param array \ * $redcapConnections REDCap connections structure - * @param array + * @param array \ * $redcapAllowedInstrumentNames Allowed instruments - * @param array> + * @param array> \ * $redcapInstrumentEventMap REDCap event-instrument mapping * * @return void From 3ed3cc2fef9aa11ae36a886dfc0ab64a44b5bbf5 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 10 Jul 2025 10:16:57 -0400 Subject: [PATCH 26/30] multiline docblock longtype test --- modules/redcap/tools/redcap_bulk_importer.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index eecc7426413..75cc6caece9 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -447,12 +447,20 @@ function testREDCapConnections( /** * Initialize REDCap instrument-event based on the given connection. * - * @param array \ - * $redcapConnections REDCap connections structure - * @param array \ - * $redcapAllowedInstrumentNames Allowed instruments - * @param array> \ - * $redcapInstrumentEventMap REDCap event-instrument mapping + * @param array{ + * int, + * RedcapHttpClient + * } $redcapConnections REDCap connections structure + * @param array{ + * string + * } $redcapAllowedInstrumentNames Allowed instruments + * @param array{ + * string, + * array{ + * int, + * RedcapInstrumentEventMap + * } + * } $redcapInstrumentEventMap REDCap event-instrument mapping * * @return void */ From 7fcc61fd19f829c5408ead96db66cc1beb75cf00 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 10 Jul 2025 10:55:55 -0400 Subject: [PATCH 27/30] multiline docblock longtype simplified types --- modules/redcap/tools/redcap_bulk_importer.php | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/modules/redcap/tools/redcap_bulk_importer.php b/modules/redcap/tools/redcap_bulk_importer.php index 75cc6caece9..b7470ef1dff 100644 --- a/modules/redcap/tools/redcap_bulk_importer.php +++ b/modules/redcap/tools/redcap_bulk_importer.php @@ -216,11 +216,11 @@ /** * Trigger the selected set of notification to import data in LORIS. * - * @param GuzzleHttp\Client $lorisClient the loris client - * @param LORIS\Database\Query $lorisDataToImport the list of data to import - * @param array> $redcapInstrumentEventMap the instrument-event map - * @param array[] $redcapConfiguration the REDCap conf - * @param string $redcapUsername the text to pass + * @param GuzzleHttp\Client $lorisClient the loris client + * @param LORIS\Database\Query $lorisDataToImport the list of data to import + * @param mixed[] $redcapInstrumentEventMap the instrument-event map + * @param mixed[] $redcapConfiguration the REDCap conf + * @param string $redcapUsername the text to pass * * @return void */ @@ -339,9 +339,11 @@ function triggerNotifications( /** * Initialize REDCap connections based on the given configuration. * - * @param array[] $redcapConfig REDCap configuration structure - * @param array $redcapConnections REDCap connection structure to fill - * @param bool $verbose Verbose mode + * @param mixed[] $redcapConfig REDCap configuration + * structure + * @param array $redcapConnections REDCap connection + * structure to fill + * @param bool $verbose Verbose mode * * @return void */ @@ -447,20 +449,14 @@ function testREDCapConnections( /** * Initialize REDCap instrument-event based on the given connection. * - * @param array{ - * int, - * RedcapHttpClient - * } $redcapConnections REDCap connections structure - * @param array{ - * string - * } $redcapAllowedInstrumentNames Allowed instruments - * @param array{ - * string, - * array{ - * int, - * RedcapInstrumentEventMap - * } - * } $redcapInstrumentEventMap REDCap event-instrument mapping + * @param array $redcapConnections REDCap + * connections + * structure + * @param array $redcapAllowedInstrumentNames Allowed + * instruments + * @param mixed[] $redcapInstrumentEventMap REDCap + * event-instrument + * mapping * * @return void */ From be177058c75e042a4ad282dca3b4b2bad8b5c4bc Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 10 Jul 2025 11:15:27 -0400 Subject: [PATCH 28/30] phan neon redcap update --- test/phpstan-loris.neon | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/phpstan-loris.neon b/test/phpstan-loris.neon index 8f0ec6edb35..8d3e99b3c04 100644 --- a/test/phpstan-loris.neon +++ b/test/phpstan-loris.neon @@ -2,3 +2,5 @@ parameters: excludes_analyse: - %currentWorkingDirectory%/modules/*/ajax/* - %currentWorkingDirectory%/modules/*/test/* + scanDirectories: + - %currentWorkingDirectory%/modules/redcap/ From 07e09c7373d1d5f31a8530ab6b0279ed7094f5b3 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 10 Jul 2025 11:33:44 -0400 Subject: [PATCH 29/30] phan neon redcap update --- test/phpstan-loris.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/phpstan-loris.neon b/test/phpstan-loris.neon index 8d3e99b3c04..63ef07b6387 100644 --- a/test/phpstan-loris.neon +++ b/test/phpstan-loris.neon @@ -3,4 +3,4 @@ parameters: - %currentWorkingDirectory%/modules/*/ajax/* - %currentWorkingDirectory%/modules/*/test/* scanDirectories: - - %currentWorkingDirectory%/modules/redcap/ + - %currentWorkingDirectory%/modules/redcap/php/** From 50176a52786c7f98ddc7d41140e834b3fbf5bb05 Mon Sep 17 00:00:00 2001 From: regisoc Date: Thu, 10 Jul 2025 11:46:23 -0400 Subject: [PATCH 30/30] phan neon redcap update --- test/phpstan-loris.neon | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/phpstan-loris.neon b/test/phpstan-loris.neon index 63ef07b6387..7ca17d7f62d 100644 --- a/test/phpstan-loris.neon +++ b/test/phpstan-loris.neon @@ -3,4 +3,5 @@ parameters: - %currentWorkingDirectory%/modules/*/ajax/* - %currentWorkingDirectory%/modules/*/test/* scanDirectories: - - %currentWorkingDirectory%/modules/redcap/php/** + - %currentWorkingDirectory%/modules/redcap/php/client/* + - %currentWorkingDirectory%/modules/redcap/php/config/*