Skip to content

Commit

Permalink
Merge pull request #91 from susom/dev
Browse files Browse the repository at this point in the history
editor feature
  • Loading branch information
sboosi authored Jun 27, 2024
2 parents f9880b4 + 267bfed commit 41f0fb2
Show file tree
Hide file tree
Showing 14 changed files with 348 additions and 563 deletions.
37 changes: 25 additions & 12 deletions Duster.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ public function redcap_every_page_top($project_id) {
&& strpos(PAGE, "index.php") !== false
&& isset($_GET['action']) && $_GET['action'] === 'create'
&& $this->isUserAllowed() === true) {
// $this->emDebug("In Every Page Top Hook project id :" . $this->getProjectId() . " Page is " . PAGE);
$some = "<script> let dusterUrl = '" . $this->getUrl("pages/newProjectIntro.php", false, true) . "' ; </script>";
echo $some;
echo "<script> const isSunet = '" . !str_contains($this->getUser()->getUsername(), '@') . "'; </script>";
echo "<script> const dusterUrl = '" . $this->getUrl("pages/newProjectIntro.php", false, true) . "' ; </script>";
$script = <<<EOD
<script>
$(document).ready(function() {
Expand All @@ -48,6 +47,10 @@ public function redcap_every_page_top($project_id) {
div += "</div>";
$("#project_template_radio0").closest('td').append(div) ;
if (!isSunet) {
$("#project_template_duster").attr('disabled', true);
$("#duster_option").append("<br><small>You must be logged into REDCap with your SUNet to use DUSTER.</small>");
}
// show DUSTER radio button option if purpose is research
$("#purpose").change(function() {
Expand Down Expand Up @@ -114,19 +117,29 @@ private function getValidToken($service) {
return $token;
}

/**
* returns the URL for the REDCap API
* @return string
*/
public function getRedcapApiUrl() {
$is_local_dev = $this->getSystemSetting('local-dev');
if($is_local_dev === true) {
return APP_PATH_WEBROOT_FULL . "api/";
/**
* returns a REDCap URL
* @param $path
* @return string
*/
public function getRedcapUrl($path = ""):string {
$server_type = $this->getSystemSetting('server-type');
$pathname = $path === "" ? $path : $path . "/";
if($server_type === "development") {
return 'https://127.0.0.1/' . $pathname;
} else {
return 'https://127.0.0.1/api/';
return APP_PATH_WEBROOT_FULL . $pathname;
}
}

/**
* returns the servername that should be used
* @return string
*/
public function getRedcapServerAlias():string {
return $this->getSystemSetting('server-type') === "development" ? '127.0.0.1' : SERVER_NAME;
}

/**
* sends a STARR-API GET request
* @param $url
Expand Down
20 changes: 6 additions & 14 deletions classes/DusterConfigClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

class DusterConfigClass
{
private $project_id, $duster_config, $module;
private $project_id, $duster_config, $design_config, $module;
private $rp_info_form = array('form_name' => 'rp_info', 'form_label' => 'Researcher-Provided Information');
private $demographics_form = array('form_name' => 'demographics', 'form_label' => 'Demographics');

Expand All @@ -30,17 +30,9 @@ public function loadConfig()
$config_url = $config_url .
((substr($config_url, -1) === '/') ? "" : "/") . SERVER_NAME . '/' . $this->project_id
. '?redcap_user=' . $this->module->getUser()->getUserName();
$this->duster_config = $this->module->starrApiGetRequest($config_url, 'ddp');
}

public function loadDesignConfig () {
// build and send GET request to config webservice
$config_url = $this->module->getSystemSetting("starrapi-config-url");
// add a '/' at the end of the url if it's not there
$config_url = $config_url .
((substr($config_url, -1) === '/') ? "" : "/") . 'design/' . SERVER_NAME . '/' . $this->project_id
. '?redcap_user=' . $this->module->getUser()->getUserName();
$this->design_config = $this->module->starrApiGetRequest($config_url, 'ddp');
$config_object = $this->module->starrApiGetRequest($config_url, 'ddp');
$this->duster_config = json_decode($config_object['config'], true);
$this->design_config = json_decode($config_object['design_config'], true);
}

public function getDusterConfig()
Expand All @@ -53,9 +45,9 @@ public function getDusterConfig()

public function getDesignConfig() {
if ($this->design_config == null) {
$this->loadDesignConfig();
$this->loadConfig();
}
return $this->design_config['design_config'];
return $this->design_config;
}

public function setDusterConfig($config)
Expand Down
2 changes: 1 addition & 1 deletion classes/RedcapToStarrLinkConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public function configureRedcapToStarrLink($em_config) {
/*constructs the url to call RtoStarr Link api
@return "1" if successful*/
private function invokeRedcapToStarrLink($action, $query) {
$url = APP_PATH_WEBROOT_FULL .
$url = $this->module->getRedcapUrl() .
'api/?type=module&prefix=redcap_to_starr_link&page=src%2FRedcapProjectToStarrLink&NOAUTH'
. '&action=' . $action // should be either data or records
. '&pid=' . $this->project_id;
Expand Down
29 changes: 25 additions & 4 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
"icon": "fas fa-receipt",
"url": "pages/populateData.php",
"show-header-and-footer": true
},
{
"name": "DUSTER: Edit Project",
"key": "dustereditproject",
"icon": "fas fa-pen-to-square",
"url": "pages/editProject.php",
"show-header-and-footer": true
}
],
"control-center": [
Expand Down Expand Up @@ -103,10 +110,24 @@
"validation" : "number"
},
{
"key": "local-dev",
"name": "<b>Local Development Environment</b><br>local development flag for REDCap API URL",
"required": false,
"type": "checkbox"
"key": "server-type",
"name": "<b>Server Type</b><br>server environment flag for REDCap URL",
"required": true,
"type": "dropdown",
"choices": [
{
"value": "local",
"name": "Local"
},
{
"value": "development",
"name": "Development"
},
{
"value": "production",
"name": "Production"
}
]
},
{
"key": "enable-allowlist",
Expand Down
44 changes: 33 additions & 11 deletions pages/editProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace Stanford\Duster;
/** @var $module Duster */

use Project;
use REDCap;

require_once $module->getModulePath() . "classes/DusterConfigClass.php";
Expand All @@ -12,39 +13,60 @@
$pid = $module->getProjectId();
$project_status = $module->getProjectStatus($pid);
$user_rights = $module->getUser()->getRights($pid);
$irb = $module->getProjectIrb($pid);
$duster_config_obj = new DusterConfigClass($pid, $module);
$design_config = $duster_config_obj->getDesignConfig();
$has_design_config = $design_config !== NULL; // PHP 8.3 provides json_validate(), which checks if a string contains valid JSON.
$editable = $project_status === "DEV"
&& strlen($user_rights['api_token']) === 32
&& $user_rights['api_import'] === '1'
&& $user_rights['design'] === '1';

if ($editable === true) {
$irb = $module->getProjectIrb($pid);
$duster_config_obj = new DusterConfigClass($pid, $module);
$design_config = json_encode($duster_config_obj->getDesignConfig());
}

&& $user_rights['design'] === '1'
&& $has_design_config === true;
?>

<!DOCTYPE html>
<html lang="en">
<h3>
DUSTER: Edit Project
</h3>
<?php if ($editable === true) {
<?php
if ($editable === true) {
?>
<p>Hitting the button below will launch an application where you may perform the following modifications to your project:</p>
Hitting the button below will launch an application where you may perform the following modifications to your project:
<ol>
<li>Add new Researcher-Provided Information.</li>
<li>Select additional demographics.</li>
<li>Add new data collection windows.</li>
<li>Add new clinical variables to pre-existing data collection windows.</li>
<!-- <li>Select additional aggregations to pre-existing clinical variables in data collection windows.</li> -->
</ol>
<strong>It is recommended you back up your project's data in case editing this project causes any issues.</strong>
<br>
Any non-DUSTER user-performed changes made to this project's current data dictionary may be lost or conflict when editing this project.
<ol>
<li>Non-DUSTER fields and forms will remain, but they may be rearranged within the data dictionary.</li>
<li>If you add a new DUSTER field and its field name matches a pre-existing non-DUSTER field, the non-DUSTER field will be replaced and its data will be overwritten when DUSTER fetches data.
<ol>Example Scenario
<li>You create a DUSTER project without selecting 'Race' under Demographics.</li>
<li>You then add a non-DUSTER field with 'race' as its REDCap field name to any of your project's instruments.</li>
<li>You subsequently edit your project via DUSTER and add 'Race' under the Demographics category.</li>
<li>Fetching data with DUSTER will save its results for 'Race' into the 'race' REDCap field, overwriting what was previously saved.</li>
</ol>
</li>
</ol>

<button
type="button"
onclick="window.location = '<?php echo $module->getUrl("pages/js/duster/new-project/dist/index.html"); ?>';"
>Launch Editor
</button>

<?php
} else if ($has_design_config === false) {
?>
<strong>
Sorry, you cannot edit this DUSTER project. This project was created before the editing feature was released and cannot be retroactively supported.
</strong>
<?php
} else {
?>
Expand Down Expand Up @@ -88,7 +110,7 @@
postObj['edit_mode'] = true;
postObj['redcap_project_id'] = "<?php echo $pid; ?>";
postObj['project_irb_number'] = "<?php echo $irb; ?>";
postObj['initial_design'] = <?php echo $design_config; ?>;
postObj['initial_design'] = <?php echo json_encode($design_config); ?>;

// store URL for REDCap's 'New Project' page
postObj['redcap_new_project_url'] = "<?php echo APP_PATH_WEBROOT_FULL . "index.php?action=create"; ?>";
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pages/js/duster/new-project/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<link rel="icon" href="./assets/favicon-db74ab0b.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DUSTER</title>
<script type="module" crossorigin src="./assets/index-66f121cc.js"></script>
<link rel="stylesheet" href="./assets/index-3c7798d2.css">
<script type="module" crossorigin src="./assets/index-39dccbbb.js"></script>
<link rel="stylesheet" href="./assets/index-28e3618b.css">
</head>
<body>
<div id="app"></div>
Expand Down
10 changes: 4 additions & 6 deletions pages/js/duster/new-project/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ const rpData = ref<BasicConfig[]>([
label:"Medical Record Number (MRN)",
redcap_field_type:"text",
value_type:"Identifier", // this needs to be replaced by "text" in review step
redcap_field_note:"8-digit number (including leading zeros, e.g., '01234567')",
redcap_field_note:"8-digit number (including leading zeros, e.g., '01234567') or 10-digit number (no leading zeros)",
phi:"t",
id: "mrn",
duster_field_name: undefined
Expand Down Expand Up @@ -255,8 +255,6 @@ const initialDesign: {[key: string]:any} = ({
collectionWindows: []
});
// const myObj: {[index: string]:any} = {}
const promptRestoreAutoSave = ref<boolean>(false);
onMounted(() => {
Expand Down Expand Up @@ -497,9 +495,8 @@ const getDusterMetadata = (metadataUrl:string) => {
};
const loadEditMode = () => {
initialDesign.value = JSON.parse(projectConfig.initial_design);
initialDesign.collectionWindows = initialDesign.value.collectionWindows;
initialDesign.value = JSON.parse(projectConfig.initial_design);
initialDesign.value = projectConfig.initial_design;
// transform and load researcher-provided data
rpData.value = initialDesign.value.rpData;
rpData.value.forEach((rp:any) => {
Expand All @@ -518,6 +515,7 @@ const loadEditMode = () => {
}
// load data collection windows
initialDesign.collectionWindows = projectConfig.initial_design.collectionWindows;
collectionWindows.value = initialDesign.value.collectionWindows;
};
Expand Down
47 changes: 45 additions & 2 deletions pages/js/duster/new-project/src/components/ReviewPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,54 @@
:label="editMode ? 'Update Project' : 'Create Project'"
icon="pi pi-check"
severity="success"
@click="editMode ? updateProject() : createProject()"
@click="editMode ? showConfirmUpdateDialog = true : createProject()"
/>
</template>
</Toolbar>
</Panel>
<Dialog
v-model:visible="showCreateProjectDialog"
:visible="showConfirmUpdateDialog"
modal
:closable="false"
:style="{ width: '50vw'}"
header="Update Project"
>
<div
>
Are you sure you want to update this project?
<br>
<strong>It is recommended you back up your project's data in case editing this project causes any issues.</strong>
<br>
Bear in mind any non-DUSTER user-performed changes made to this project's current data dictionary may be lost or conflict when editing this project.
<ol>
<li>Non-DUSTER fields and forms will remain, but they may be rearranged within the data dictionary.</li>
<li>If you add a new DUSTER field and its field name matches a pre-existing non-DUSTER field, the non-DUSTER field will be replaced and its data will be overwritten when DUSTER fetches data.
<ol>Example Scenario
<li>You create a DUSTER project without selecting 'Race' under Demographics.</li>
<li>You then add a non-DUSTER field with 'race' as its REDCap field name to any of your project's instruments.</li>
<li>You subsequently edit your project via DUSTER and add 'Race' under the Demographics category.</li>
<li>Fetching data with DUSTER will save its results for 'Race' into the 'race' REDCap field, overwriting what was previously saved.</li>
</ol>
</li>
</ol>
</div>
<template #footer>
<Button
label="No, Cancel"
severity="secondary"
@click="showConfirmUpdateDialog = false"
autofocus
/>
<Button
label="Yes, Update"
severity="primary"
@click="updateProject()"
autofocus
/>
</template>
</Dialog>
<Dialog
:visible="showCreateProjectDialog"
modal
:closable="false"
:style="{ width: '50vw' }"
Expand Down Expand Up @@ -651,6 +692,7 @@ const getScoresConfig = (scoresMeta:FieldMetadata[], index:number) => {
}
const showCreateProjectDialog = ref<boolean>(false);
const showConfirmUpdateDialog = ref<boolean>(false);
const createProjectMessage = ref<string>("");
const createProjectError = ref<boolean>(false);
const systemErrorMessage = ref<string>("");
Expand Down Expand Up @@ -753,6 +795,7 @@ const createProject = () => {
}
const updateProject = () => {
showConfirmUpdateDialog.value = false;
createProjectMessage.value = "Updating REDCap Project. Please wait.";
showCreateProjectDialog.value = true;
const data = {
Expand Down
2 changes: 1 addition & 1 deletion services/createProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
);

$ch = curl_init();
$api_url = $module->getRedcapApiUrl();
$api_url = $module->getRedcapUrl("api");

$module->emDebug("Create Project POST Request to REDCap API URL $api_url");
curl_setopt($ch, CURLOPT_URL, $api_url);
Expand Down
Loading

0 comments on commit 41f0fb2

Please sign in to comment.