Skip to content

Commit

Permalink
Merge branch 'release/1.3.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
pbchase committed Feb 20, 2024
2 parents 0510676 + 830feff commit 5c0cd84
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 63 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Change Log
All notable changes to Move Data to Other Event will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).


## [1.3.0] - 2024-02-20
### Added
- Added support for REDCap 14.0.0 data tables (@ChemiKyle)

### Changed
- Update to EM framework 15 (@ChemiKyle)
- Wrap mdoe.js in IIFE (@ChemiKyle)


## [1.2.0] - 2021-02-17
### Changed
- Wrap record_id = * in quotes to allow for non-integer record_id values (Kyle Chesney)
Expand Down
92 changes: 63 additions & 29 deletions ExternalModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ function redcap_every_page_top($project_id) {

$project_settings = $this->framework->getProjectSettings();

if (!$project_settings['active']['value']) {
return;
}
if (!$project_settings['active']) return;

// needed to bypass uncatchable exception in framework->getUser()
if ( !defined('USERID') ) return;

if ( !$this->framework->getUser()->hasDesignRights() &&
( $this->getSystemSetting('restrict_to_designers_global') ||
!$project_settings['allow_non_designers']['value']) )
!$project_settings['allow_non_designers']) )
{
return;
}
Expand All @@ -42,23 +40,43 @@ function includeJs($path) {
echo '<script src="' . $this->getUrl($path) . '">;</script>';
}

// Equivalent to fetch_all($mysqli_result, MYSQLI_ASSOC)
// REDCap EM framework 15's query result objects do not support fetch_all
private function unspoolMysqliResult($mysqli_result) {
$data = [];
for ($i = 0; $i < $mysqli_result->num_rows; ++$i) {
$data[$i] = $mysqli_result->fetch_assoc();
}

return $data;
}

function moveEvent($source_event_id, $target_event_id, $record_id = NULL, $project_id = NULL, $form_names = NULL, $delete_source_data = true) {
$record_id = $record_id ?: ( ($this->framework->getRecordId()) ?: NULL ); // return in place of NULL causes errors
$project_id = $project_id ?: ( ($this->framework->getProjectId()) ?: NULL );
$record_pk = REDCap::getRecordIdField();
$form_names = implode("', '", $form_names);
$redcap_data_table = REDCap::getDataTable($project_id);
// HACK: whitelist potential table names since they cannot be parameterized
if (!preg_match("/^redcap_data\d*$/", $redcap_data_table)) { return; }

//TODO: sanitize without mysqli_real_escape_string
$sql = "SELECT a.field_name FROM redcap_metadata as a
INNER JOIN (SELECT form_name FROM redcap_events_forms WHERE event_id = " . ($source_event_id) . ")
INNER JOIN (SELECT form_name FROM redcap_events_forms WHERE event_id = ?)
as b ON a.form_name = b.form_name
WHERE a.project_id = " . ($project_id) . "
AND a.form_name IN ('" . $form_names . "')
ORDER BY field_order ASC;";
WHERE a.project_id = ?";

$fields = [];
$result= $this->framework->query($sql);
$sql_parameters = [$source_event_id, $project_id];

$query = $this->framework->createQuery();
$query->add($sql, $sql_parameters);
$query->add('and')->addInClause('a.form_name', $form_names);
$query->add("ORDER BY field_order ASC");

$result = $query->execute();

// Needed for logging
$form_names = implode("', '", $form_names);

$fields = [];
while ($row = $result->fetch_assoc()) {
$fields[] = $row["field_name"];
}
Expand All @@ -71,9 +89,9 @@ function moveEvent($source_event_id, $target_event_id, $record_id = NULL, $proje
'events' => $source_event_id
];

$field_list = ($fields) ? " AND d.field_name IN ('" . implode('\',\'', $fields) . "');" : ";";
// NOTE: prepared statements can not parameterize table names
$edocs_sql = "SELECT d.field_name, em.doc_id, em.stored_name, em.doc_name
FROM redcap_data d
FROM $redcap_data_table d
INNER JOIN redcap_metadata m
ON
m.project_id = d.project_id
Expand All @@ -82,18 +100,21 @@ function moveEvent($source_event_id, $target_event_id, $record_id = NULL, $proje
INNER JOIN redcap_edocs_metadata em
ON em.doc_id = d.value
WHERE
d.project_id = " . $project_id . "
AND d.record = '" . $record_id . "'
AND d.event_id = " . $source_event_id .
$field_list;
d.project_id = ?
AND d.record = ?
AND d.event_id = ?";

// TODO: consider: em.element_validation_type == 'signature'
$parameters = [$project_id, $record_id, $source_event_id];

$edocs_fields = $this->framework->query($edocs_sql);
$query = $this->framework->createQuery();
$query->add($edocs_sql, $parameters);
$query->add('and')->addInClause('d.field_name', $fields);
$edocs_fields = $query->execute();

$edocs_present = ($edocs_fields->num_rows > 0);
if ($edocs_present) {
$edocs_results = $edocs_fields->fetch_all(MYSQLI_ASSOC);
$edocs_results = $this->unspoolMysqliResult($edocs_fields);
}

$new_data = [];
Expand Down Expand Up @@ -154,27 +175,40 @@ function moveEvent($source_event_id, $target_event_id, $record_id = NULL, $proje
function forceMigrateSourceFields($get_data, $project_id, $record_id, $source_event_id, $target_event_id, $log_message) {
$check_old = REDCap::getData($get_data)[$record_id][$source_event_id];

$redcap_data_table = REDCap::getDataTable($project_id);
// HACK: whitelist potential table names since they cannot be parameterized
if (!preg_match("/^redcap_data\d*$/", $redcap_data_table)) return;

// check for fields which did not transfer
$revisit_fields = [];
foreach ($check_old as $field => $value) {
if ($value !== '' && $value !== '0' && $value !== NULL &&
$field != REDCap::getRecordIdField()) {
array_push($revisit_fields, "'$field'");
array_push($revisit_fields, $field);
}
}

if ($revisit_fields !== []) {
// Raw SQL to transfer docs which do not transfer or delete with saveData
// explicitly excluding the record's primary key
$revisit_fields = implode(',', $revisit_fields);

$docs_xfer_sql = "UPDATE $redcap_data_table SET event_id = ?
WHERE project_id = ?
AND event_id = ?
AND record = ?
AND field_name NOT IN (?)";

$record_id_field = "'" . REDCap::getRecordIdField() . "'";
$docs_xfer_parameters = [$target_event_id, $project_id, $source_event_id, $record_id, $record_id_field];

$query = $this->framework->createQuery();
$query->add($docs_xfer_sql, $docs_xfer_parameters);
$query->add('and')->addInClause('field_name', $revisit_fields);

$query->execute();

$revisit_fields = implode("', '", $revisit_fields);
$log_message .= ". Forced transfer of additional field(s): " . $revisit_fields;
$docs_xfer_sql = "UPDATE redcap_data SET event_id = " . $target_event_id . "
WHERE project_id = " . $project_id . "
AND event_id = " . $source_event_id . "
AND record = '" . $record_id . "'
AND field_name NOT IN ('" . REDCap::getRecordIdField() . "')
AND field_name IN (" . $revisit_fields . ");";
$this->framework->query($docs_xfer_sql);
}
return $log_message;
}
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.0
1.3.0
27 changes: 4 additions & 23 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,11 @@
"name": "Move Data to Other Event",
"description": "Allow privileged users to easily move data, uploaded files, and signatures between events.",
"namespace": "MDOE\\ExternalModule",
"framework-version": 3,
"permissions": [
"redcap_every_page_top",
"redcap_save_record"
],
"framework-version": 15,
"authors": [
{
"name": "Kyle Chesney",
"email": "[email protected]",
"institution": "University of Florida - CTSI"
},
{
"name": "Michael Bentz",
"email": "[email protected]",
"institution": "University of Florida - CTSI"
},
{
"name": "Taryn Stoffs",
"email": "[email protected]",
"institution": "University of Florida - CTSI"
},
{
"name": "Philip Chase",
"email": "[email protected]",
"name": "University of Florida CTS-IT",
"email": "[email protected]",
"institution": "University of Florida - CTSI"
}
],
Expand All @@ -49,6 +30,6 @@
}
],
"compatibility": {
"redcap-version-min": "9.3.0"
"redcap-version-min": "14.0.2"
}
}
14 changes: 11 additions & 3 deletions js/mdoe.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Wrap in immediately invoked function expression to retain global scope purity
(() => {

let module = ExternalModules['MDOE'].ExternalModule;

let dialogButton = $( '<i class="fas fa-truck" type="image" style="padding: 5px; cursor: pointer;"/>' );
Expand Down Expand Up @@ -152,7 +155,10 @@ document.addEventListener('DOMContentLoaded', function() {
$(dialogEvent).parent().find(".ui-dialog-buttonpane").hide();
}

selectedColValues.css("background-color", "#ff9933");
// highlight column of source event
// append to initial style and mark important to override tr-parity
// https://stackoverflow.com/a/2655976/7418735
selectedColValues.attr('style', (i,s) => { return (s || '') + 'background-color: #ff9933 !important;' });

dialogEvent
.on('dialogclose', function(event) { selectedColValues.css("background-color", ""); })
Expand Down Expand Up @@ -218,8 +224,8 @@ document.addEventListener('DOMContentLoaded', function() {
$(dialogForm).parent().find(".ui-dialog-buttonpane").hide();
}

//highlight cell of source form
thisCell.css("background-color", "#ff9933");
// highlight cell of source form
thisCell.attr('style', (i,s) => { return (s || '') + 'background-color: #ff9933 !important;' });

dialogForm
.on('dialogclose', function(event) { thisCell.css("background-color", ""); })
Expand Down Expand Up @@ -258,3 +264,5 @@ function ajaxMoveEvent(sourceEventId, targetEventId, formNames = null, deleteSou
//}
});
}

})()
9 changes: 2 additions & 7 deletions migratedata.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<?php

namespace MDOE\ExternalModule;
require_once(__DIR__ . '/ExternalModule.php');

$migrating = $_REQUEST["migrating"];
$form_names = $_REQUEST["formNames"];
$source_event_id = $_REQUEST["sourceEventId"];
Expand All @@ -11,12 +8,10 @@
$project_id = $_REQUEST["projectId"];
$delete_source_data = $_REQUEST["deleteSourceData"];

$EM = new ExternalModule();

//TODO: eliminate this switch
//TODO: consider elimination of this switch if ajaxMoveEvent will never deliver values other than "event"
switch ($migrating) {
case 'event':
$response = $EM->moveEvent($source_event_id, $target_event_id, $record_id, $project_id, $form_names, $delete_source_data == "true");
$response = $module->moveEvent($source_event_id, $target_event_id, $record_id, $project_id, $form_names, $delete_source_data == "true");
break;
case 'field':
echo "not implemented";
Expand Down

0 comments on commit 5c0cd84

Please sign in to comment.