diff --git a/DataEntryTriggerBuilder.php b/DataEntryTriggerBuilder.php index e8f6c1b..269ce70 100644 --- a/DataEntryTriggerBuilder.php +++ b/DataEntryTriggerBuilder.php @@ -8,12 +8,12 @@ use REDCap; use Project; -class DataEntryTriggerBuilder extends \ExternalModules\AbstractExternalModule +class DataEntryTriggerBuilder extends \ExternalModules\AbstractExternalModule { /** * Replaces all strings in $text with $replacement * So "Alice says 'hello'" becomes "Alice says ''" assuming $replacement = ''. - * + * * @access private * @param String $text The text to replace. * @param String $replacement The replacement text. @@ -21,8 +21,11 @@ class DataEntryTriggerBuilder extends \ExternalModules\AbstractExternalModule */ private function replaceStrings($text, $replacement) { + $quotes = []; + preg_match_all("/'/", $text, $quotes, PREG_OFFSET_CAPTURE); $quotes = $quotes[0]; + if (sizeof($quotes) % 2 === 0) { $i = 0; @@ -32,15 +35,15 @@ private function replaceStrings($text, $replacement) $to_replace[] = substr($text, $quotes[$i][1], $quotes[$i + 1][1] - $quotes[$i][1] + 1); $i = $i + 2; } - + $text = str_replace($to_replace, $replacement, $text); } return $text; } - + /** * Parses a syntax string into blocks. - * + * * @access private * @param String $syntax The syntax to parse. * @return Array An array of blocks that make up the syntax passed. @@ -49,10 +52,10 @@ private function getSyntaxParts($syntax) { $syntax = str_replace(array("['", "']"), array("[", "]"), $syntax); $syntax = $this->replaceStrings(trim($syntax), "''"); // Replace strings with '' - + $parts = array(); $previous = array(); - + $i = 0; while($i < strlen($syntax)) { @@ -105,13 +108,13 @@ private function getSyntaxParts($syntax) break; } } - + return $parts; } - + /** * Checks whether a field exists within a project. - * + * * @param String $var The field to validate * @param String $pid The project id the field supposedly belongs to. Use current project if null. * @return Boolean true if field exists, false otherwise. @@ -126,22 +129,38 @@ public function isValidField($var, $pid = null) else { $data_dictionary = REDCap::getDataDictionary('array'); } - + $fields = array_keys($data_dictionary); - + $external_fields = array(); + $external_fields[] = "redcap_data_access_group"; $instruments = array_unique(array_column($data_dictionary, "form_name")); foreach ($instruments as $unique_name) - { + { $external_fields[] = "{$unique_name}_complete"; } - return in_array($var, $external_fields) || in_array($var, $fields); + $checkbox_values = array(); + foreach($data_dictionary as $field_name => $data) + { + if ($data["field_type"] == "checkbox") + { + $choices = explode("|", $data["select_choices_or_calculations"]); + foreach($choices as $choice) + { + $choice = trim($choice); + $code = trim(substr($choice, 0, strpos($choice, ","))); + $checkbox_values[] = "{$field_name}___{$code}"; + } + } + } + + return in_array($var, $external_fields) || in_array($var, $fields) || in_array($var, $checkbox_values); } - + /** * Checks whether a event exists within a project. - * + * * @param String $var The event to validate * @param String $pid The project id the event supposedly belongs to. Use current project if null. * @return Boolean true if event exists, false otherwise. @@ -149,14 +168,14 @@ public function isValidField($var, $pid = null) public function isValidEvent($var, $pid = null) { $var = trim($var, "'"); - $Proj = new Project($pid); - $events = array_values($Proj->getUniqueEventNames()); + $RedcapProj = new Project($pid); + $events = array_values($RedcapProj->getUniqueEventNames()); return in_array($var, $events); } - + /** * Checks whether a instrument exists within a project. - * + * * @param String $var The instrument to validate * @param String $pid The project id the instrument supposedly belongs to. Use current project if null. * @return Boolean true if instrument exists, false otherwise. @@ -171,15 +190,15 @@ public function isValidInstrument($var, $pid = null) else { $data_dictionary = REDCap::getDataDictionary('array'); } - + $instruments = array_unique(array_column($data_dictionary, "form_name")); return in_array($var, $instruments); } - + /** * Validate syntax. - * + * * @access private * @see Template::getSyntaxParts() For retreiving blocks of syntax from the given syntax string. * @param String $syntax The syntax to validate. @@ -189,43 +208,43 @@ public function isValidInstrument($var, $pid = null) public function validateSyntax($syntax) { $errors = array(); - + $logical_operators = array("==", "<>", "!=", ">", "<", ">=", ">=", "<=", "<=", "||", "&&", "="); $parts = $this->getSyntaxParts($syntax); - + $opening_squares = array_keys($parts, "["); $closing_squares = array_keys($parts, "]"); - + $opening_parenthesis = array_keys($parts, "("); $closing_parenthesis = array_keys($parts, ")"); - + // Check symmetry of () if (sizeof($opening_parenthesis) != sizeof($closing_parenthesis)) { $errors[] = "ERROROdd number of parenthesis (. You've either added an extra parenthesis, or forgot to close one."; } - + // Check symmetry of [] if (sizeof($opening_squares) != sizeof($closing_squares)) { $errors[] = "Odd number of square brackets [. You've either added an extra bracket, or forgot to close one."; } - + foreach($parts as $index => $part) { switch ($part) { case "(": $previous = $parts[$index - 1]; $next_part = $parts[$index + 1]; - - if ($next_part !== "(" - && $next_part !== ")" + + if ($next_part !== "(" + && $next_part !== ")" && $next_part !== "[" && !is_numeric($next_part) - && $next_part[0] != "'" + && $next_part[0] != "'" && $next_part[0] != "\"" - && $next_part[strlen($next_part) - 1] != "'" + && $next_part[strlen($next_part) - 1] != "'" && $next_part[strlen($next_part) - 1] != "\"") { $errors[] = "Invalid $next_part after (."; @@ -243,7 +262,7 @@ public function validateSyntax($syntax) } break; case "==": - case "<>": + case "<>": case "!=": case ">": case "<": @@ -260,17 +279,17 @@ public function validateSyntax($syntax) { $previous = $parts[$index - 2]; $next_part = $parts[$index + 1]; - + if (in_array($previous, $logical_operators) && $previous !== "or" && $previous !== "and") { $errors[] = "Invalid $part. You cannot chain comparison operators together, you must use an and or an or"; } - - if (!empty($next_part) + + if (!empty($next_part) && !is_numeric($next_part) - && $next_part[0] != "'" + && $next_part[0] != "'" && $next_part[0] != "\"" - && $next_part[strlen($next_part) - 1] != "'" + && $next_part[strlen($next_part) - 1] != "'" && $next_part[strlen($next_part) - 1] != "\"") { $errors[] = "Invalid $next_part after $part."; @@ -290,8 +309,8 @@ public function validateSyntax($syntax) else if ($index != sizeof($parts) - 1) { $next_part = $parts[$index + 1]; - if (!empty($next_part) - && $next_part !== "(" + if (!empty($next_part) + && $next_part !== "(" && $next_part !== "[") { $errors[] = "Invalid $next_part after $part."; @@ -311,13 +330,13 @@ public function validateSyntax($syntax) $previous_2 = $parts[$index - 2]; $previous_5 = $parts[$index - 5]; $next_part = $parts[$index + 1]; - + if ($previous_2 !== "[" && $previous_5 !== "[") // Make sure it has an opening bracket. Proper syntax should be [, field_name, ], or [, field_name, (, code, ), ] { $errors[] = "Unclosed or empty ] bracket."; } - - if ($next_part !== ")" + + if ($next_part !== ")" && $next_part !== "[" && !in_array($next_part, $logical_operators)) { @@ -327,33 +346,33 @@ public function validateSyntax($syntax) break; default: // Check if it's a field or event - if ($part[0] != "'" && - $part[0] != "\"" && - $part[strlen($part) - 1] != "'" && - $part[strlen($part) - 1] != "\"" && - !is_numeric($part) && - !empty($part) && - ($this->isValidField($part) == false && $this->isValidEvent($part) == false)) + if ($part[0] != "'" && + $part[0] != "\"" && + $part[strlen($part) - 1] != "'" && + $part[strlen($part) - 1] != "\"" && + !is_numeric($part) && + !empty($part) && + ($this->isValidField($part) == false && $this->isValidEvent($part) == false)) { $errors[] = "$part is not a valid event/field in this project. If this is a checkbox field please use the following format: field_name(code)"; } break; } } - + return $errors; } /** * Retrieve the following for all REDCap projects: ID, & title - * + * * @return Array An array of rows pulled from the database, each containing a project's information. */ public function getProjects() { $query = $this->framework->createQuery(); $query->add("select project_id, app_title from redcap_projects", []); - + if ($query_result = $query->execute()) { while($row = $query_result->fetch_assoc()) @@ -363,35 +382,47 @@ public function getProjects() } return $projects; } - + /** * Retrieves a project's fields - * + * * @param String $pid A project's id in REDCap. - * @return String A JSON encoded string that contains all the instruments and fields for a project. + * @return String A JSON encoded string that contains all the event, instruments, and fields for a project. */ public function retrieveProjectMetadata($pid) { if (!empty($pid)) { $metadata = REDCap::getDataDictionary($pid, "array"); - $instruments = array_unique(array_column($metadata, "form_name")); - $Proj = new Project($pid); - $events = array_values($Proj->getUniqueEventNames()); - $isLongitudinal = $Proj->longitudinal; + $instruments = array_values(array_unique(array_column($metadata, "form_name"))); + $RedcapProj = new Project($pid); + $events = array_values($RedcapProj->getUniqueEventNames()); + $isLongitudinal = $this->escape($RedcapProj->longitudinal); + /** - * We can pipe over any data except descriptive fields. - * + * We can pipe over any data except descriptive, file, and signature fields. + * * NOTE: For calculation fields only the raw data can be imported/exported. */ foreach($metadata as $field_name => $data) { - if ($data["field_type"] != "descriptive" && $data["field_type"] != "calc") + if ($data["field_type"] == "checkbox") + { + $choices = explode("|", $data["select_choices_or_calculations"]); + foreach($choices as $choice) + { + $choice = trim($choice); + $code = trim(substr($choice, 0, strpos($choice, ","))); + $fields[] = "{$field_name}___{$code}"; + } + + } + if ($data["field_type"] != "descriptive" && $data["field_type"] != "signature" && $data["field_type"] != "file") { $fields[] = $field_name; } } - + /** * Add form completion status fields to push */ @@ -399,12 +430,31 @@ public function retrieveProjectMetadata($pid) { $fields[] = $instrument . "_complete"; } - - return ["fields" => $fields, "events" => $events, "isLongitudinal" => $isLongitudinal]; + + // Add redcap_data_access_group to fields + $fields[] = "redcap_data_access_group"; + + return ["fields" => $fields, "events" => $events, "isLongitudinal" => $isLongitudinal, "instruments" => $instruments]; } return FALSE; } + /** + * Retrieves a project's data access groups + * + * @param String $pid A project's id in REDCap + * @return Array An array of unique Data Access Group names (based upon group name text) with group_id as array key and unique name as element + */ + public function retrieveProjectGroups($pid) + { + $RedcapProj = new Project($pid); + $dags = $RedcapProj->getUniqueGroupNames(); + if ($dags) { + return $dags; + } + return FALSE; + } + /** * REDCap hook is called immediately after a record is saved. Will retrieve the DET settings, * & import data according to DET. @@ -414,56 +464,69 @@ public function redcap_save_record($project_id, $record, $instrument, $event_id, if ($project_id == $this->getProjectId()) { // Get DET settings - $settings = json_decode($this->getProjectSetting("det_settings"), true); - - $dest_project = $settings["dest-project"]; - $create_record_trigger = $settings["create-record-cond"]; - - $link_source_event = $settings["linkSourceEvent"]; - $link_source = $settings["linkSource"]; - - $link_dest_event = $settings["linkDestEvent"]; - $link_dest_field = $settings["linkDest"]; - - $triggers = $settings["triggers"]; + $settings_json = $this->getProjectSetting("det_settings"); - $piping_source_events = $settings["pipingSourceEvents"]; - $piping_dest_events = $settings["pipingDestEvents"]; - - $piping_source_fields = $settings["pipingSourceFields"]; - $piping_dest_fields = $settings["pipingDestFields"]; + if (!$settings_json || $settings_json == "null") { + return; + } - $set_dest_events = $settings["setDestEvents"]; - $set_dest_fields = $settings["setDestFields"]; - $set_dest_fields_values = $settings["setDestFieldsValues"]; + $settings = json_decode($settings_json, true); - $source_instruments_events = $settings["sourceInstrEvents"]; - $source_instruments = $settings["sourceInstr"]; + if ($settings) + { + if (array_key_exists("triggers", $settings)) { + $triggers = $settings["triggers"]; + } + else { + $triggers = $settings; + } - $overwrite_data = $settings["overwrite-data"]; - $import_dags = $settings["import-dags"]; + if (array_filter($triggers, 'is_array') !== $triggers) { + $this->log("DET Builder: Existing settings not in expected json format for module"); + return; + } + } + else { + $this->log("DET Builder: Invalid json detected in existing settings"); + return; + } // Get current record data - $record_data = json_decode(REDCap::getData("json", $record, null, null, null, false, $import_dags), true); - + $record_data = json_decode(REDCap::getData("json", $record, null, null, null, false, true), true); + /** * Process each trigger, and, if true, prepare associated data to move. */ - foreach($triggers as $index => $trigger) + foreach($triggers as $index => $trigger_obj) { - $valid = REDCap::evaluateLogic($trigger, $project_id, $record); // REDCap class method to evaluate conditional logic. + $valid = REDCap::evaluateLogic($trigger_obj["trigger"], $project_id, $record); // REDCap class method to evaluate conditional logic. if ($valid === null) // Null returned if logic is invalid. Else a boolean value. { - REDCap::logEvent("DET: Trigger was either syntactically incorrect, or parameters were invalid (e.g., record or event does not exist). No data moved.", "Trigger: $trigger", null, $record, $event_id, $project_id); + $this->log("DET Builder: Trigger was either syntactically incorrect, or parameters were invalid (e.g., record or event does not exist). No data moved. Trigger #" . ($index + 1) . ": " . $trigger_obj["trigger"]); } else if ($valid) { - $trigger_source_fields = $piping_source_fields[$index]; - $trigger_source_events = $piping_source_events[$index]; - - $trigger_dest_fields = $piping_dest_fields[$index]; - $trigger_dest_events = $piping_dest_events[$index]; - + $dest_record_data = []; + + $dest_project = $trigger_obj["dest-project"]; + + $dest_dags = $this->retrieveProjectGroups($dest_project); + + $overwrite_data = $trigger_obj["overwrite-data"]; + $import_dags = $trigger_obj["import-dags"]; + + $link_source_event = $trigger_obj["linkSourceEvent"]; + $link_source = $trigger_obj["linkSource"]; + + $link_dest_event = $trigger_obj["linkDestEvent"]; + $link_dest_field = $trigger_obj["linkDest"]; + + $trigger_source_fields = $trigger_obj["pipingSourceFields"]; + $trigger_source_events = $trigger_obj["pipingSourceEvents"]; + + $trigger_dest_fields = $trigger_obj["pipingDestFields"]; + $trigger_dest_events = $trigger_obj["pipingDestEvents"]; + /** * Move field data from source to destination */ @@ -479,640 +542,767 @@ public function redcap_save_record($project_id, $record, $instrument, $event_id, { $data = $record_data[0]; // Takes data from first event } - + if (!empty($trigger_dest_events[$i])) { $dest_event = $trigger_dest_events[$i]; } - else + else // Assume classic project { - $dest_event = "event_1_arm_1"; // Assume classic project and use event_1_arm_1 + $dest_event = "classic"; } - if (empty($dest_record_data[$dest_event])) // Create entry for event if it doesn't already exist. + if (empty($dest_record_data[$dest_event])) { - $event_data = ["redcap_event_name" => $dest_event]; + $event_data = []; } else { $event_data = $dest_record_data[$dest_event]; } - + $source_field = $trigger_source_fields[$i]; - $event_data[$dest_field] = $data[$source_field]; + + if ($dest_field == "redcap_data_access_group") { + $unique_group_name = $dest_dags[$data[$source_field]]; + $value = $unique_group_name; + } + else { + $value = $data[$source_field]; + } + + $event_data[$dest_field] = $value; $dest_record_data[$dest_event] = $event_data; } - + /** * Set destination fields as custom value */ - $trigger_dest_fields = $set_dest_fields[$index]; - $trigger_dest_values = $set_dest_fields_values[$index]; - $trigger_dest_events = $set_dest_events[$index]; - + $trigger_dest_fields = $trigger_obj["setDestFields"]; + $trigger_dest_values = $trigger_obj["setDestFieldsValues"]; + $trigger_dest_events = $trigger_obj["setDestEvents"]; + foreach($trigger_dest_fields as $i => $dest_field) { if (!empty($trigger_dest_events[$i])) { $dest_event = $trigger_dest_events[$i]; } - else + else // Assume classic project { - $dest_event = "event_1_arm_1"; + $dest_event = "classic"; } - + if (empty($dest_record_data[$dest_event])) { - $event_data = ["redcap_event_name" => $dest_event]; + $event_data = []; } else { $event_data = $dest_record_data[$dest_event]; } + + if ($dest_field == "redcap_data_access_group") { + $unique_group_name = $dest_dags[$trigger_dest_values[$i]]; + $value = $unique_group_name; + } + else { + $value = $trigger_dest_values[$i]; + } - $event_data[$dest_field] = $trigger_dest_values[$i]; + $event_data[$dest_field] = $value; $dest_record_data[$dest_event] = $event_data; } - + /** - * Move source instruments to destination instruments (Is a one-to-one relationship). + * Move source instruments to destination instruments. */ - $trigger_source_instruments = $source_instruments[$index]; - $trigger_source_instruments_events = $source_instruments_events[$index]; + $trigger_source_instruments = $trigger_obj["sourceInstr"]; + $trigger_source_instruments_events = $trigger_obj["sourceInstrEvents"]; + $trigger_dest_instruments_events = $trigger_obj["destInstrEvents"]; + + $dest_dd = REDCap::getDataDictionary($dest_project, "array"); + $dest_fields = array_keys($dest_dd); + + // Add form completion statuses as fields to consider in the destination project + $dest_form_completion_fields = array_map(function($value) { + return $value . "_complete"; + }, + array_unique(array_column(array_values($dest_dd), "form_name"))); + + $dest_fields = array_merge($dest_fields, $dest_form_completion_fields); + foreach($trigger_source_instruments as $i => $source_instrument) { - if (!empty($trigger_source_instruments_events[$i])) + if (!empty($trigger_dest_instruments_events[$i])) { - $event = $trigger_source_instruments_events[$i]; + $dest_event = $trigger_dest_instruments_events[$i]; } - else + else // Assume destination is classic project. { - $event = "event_1_arm_1"; + $dest_event = "classic"; } - - if (empty($dest_record_data[$event])) + + if (empty($dest_record_data[$dest_event])) { - $event_data = ["redcap_event_name" => $event]; + $event_data = []; } else { - $event_data = $dest_record_data[$event]; + $event_data = $dest_record_data[$dest_event]; } - + // Fields are returned in the order they are in the REDCap project $source_instrument_fields = REDCap::getFieldNames($source_instrument); - $source_instrument_data = json_decode(REDCap::getData("json", $record, $source_instrument_fields, $event), true); + // Add completion status to move with instrument + $source_instrument_fields[] = $source_instrument . "_complete"; + + // Check for fields that don't exist in the destination project, and remove them + $source_instrument_fields = array_filter($source_instrument_fields, function($v, $k) use ($dest_fields) { + return in_array($v, $dest_fields); + }, ARRAY_FILTER_USE_BOTH); + + if (!empty($source_instrument_fields)) { + $source_event = !empty($trigger_source_instruments_events[$i]) ? $trigger_source_instruments_events[$i] : null; + $source_instrument_data = json_decode(REDCap::getData("json", $record, $source_instrument_fields, $source_event), true); + } + if (sizeof($source_instrument_data) > 0) { - $event_data = $event_data + $source_instrument_data[0]; - $dest_record_data[$event] = $event_data; + // Remove any REDcap repeat instance fields, as module is currently incompatible + $source_instrument_data = $source_instrument_data[0]; + unset($source_instrument_data["redcap_repeat_instrument"], $source_instrument_data["redcap_repeat_instance"]); + $event_data = $event_data + $source_instrument_data; + $dest_record_data[$dest_event] = $event_data; } } - } - } - - if (!empty($dest_record_data)) { - // Check if the linking id field is the same as the record id field. - $dest_record_id = $this->framework->getRecordIdField($dest_project); - if ($dest_record_id != $link_dest_field) - { - /** - * Check for existing record, otherwise create a new one. Assume linking ID is unique. - */ - - // Search for the index of the linking id's event. If not found, then assume it's a classical project and that the index for the first event is 0. - if (!empty($link_source_event)) + + if (!empty($dest_record_data) || $trigger_obj["create-empty-record"] == 1) { - $key = array_search($link_source_event, array_column($record_data, "redcap_event_name")); - } - else - { - $key = 0; - } - - $data = $record_data[$key]; - $link_dest_value = $data[$link_source]; - - // Set linking id - if (!empty($link_dest_event)) - { - $dest_record_data[$link_dest_event][$link_dest_field] = $link_dest_value; - } - else - { - $dest_record_data["event_1_arm_1"][$link_dest_field] = $link_dest_value; - } - - // Retrieve record id. Exit is there is no value for the linking field, as it should be filled and never change. - if (empty($link_dest_value)) - { - REDCap::logEvent("DET: Linking field value is empty, so no data moved", "Filter logic: [$link_dest_field] = ''", null, $record, $event_id, $project_id); - return; - } - else - { - $filter_logic = "[$link_dest_field] = '$link_dest_value'"; - $existing_record = REDCap::getData($dest_project, "json", null, $dest_record_id, $link_dest_event, null, false, false, false, $filter_logic); - $existing_record = json_decode($existing_record, true); - - if (sizeof($existing_record) == 0) + if (empty($dest_record_data)) { - $dest_record = $this->framework->addAutoNumberedRecord($dest_project); + if (empty($link_dest_event)) + $dest_record_data["classic"] = []; + else + $dest_record_data[$link_dest_event] = []; + } + + // Check if the linking id field is the same as the record id field. + $dest_record_id = $this->framework->getRecordIdField($dest_project); + if ($dest_record_id != $link_dest_field) + { + /** + * Check for existing record, otherwise create a new one. Assume linking ID is unique. + */ + + // Search for the index of the linking id's event. If not found, then assume it's a classical project and that the index for the first event is 0. + if (!empty($link_source_event)) + { + $key = array_search($link_source_event, array_column($record_data, "redcap_event_name")); + } + else + { + $key = 0; + } + + $data = $record_data[$key]; + $link_dest_value = $data[$link_source]; + + if (!empty($trigger_obj["prefixPostfixStr"])) + { + if ($trigger_obj["prefixOrPostfix"] == "post") + $link_dest_value .= $trigger_obj["prefixPostfixStr"]; + else + $link_dest_value = $trigger_obj["prefixPostfixStr"] . $link_dest_value; + } + + // Set linking id + if (!empty($link_dest_event)) + { + $dest_record_data[$link_dest_event][$link_dest_field] = $link_dest_value; + } + else // Assume classic project + { + $dest_record_data["classic"][$link_dest_field] = $link_dest_value; + } + + // Retrieve record id. Exit if there is no value for the linking field, as it should be filled and never change. + if (empty($link_dest_value)) + { + $this->log("DET Builder: Linking field value is empty, so no data moved. Filter logic: [$link_dest_field] = ''"); + return; + } + else + { + $filter_logic = "[$link_dest_field] = '$link_dest_value'"; + $existing_record = REDCap::getData($dest_project, "json", null, $dest_record_id, $link_dest_event, null, false, false, false, $filter_logic); + $existing_record = json_decode($existing_record, true); + + if (sizeof($existing_record) == 0) + { + $dest_record = REDCap::reserveNewRecordId($dest_project); + } + else + { + $dest_record = $existing_record[0][$dest_record_id]; + } + } } else { - $dest_record = $existing_record[0][$dest_record_id]; + $dest_record = $record_data[0][$link_source]; + if (!empty($trigger_obj["prefixPostfixStr"])) + { + if ($trigger_obj["prefixOrPostfix"] == "post") + $dest_record .= $trigger_obj["prefixPostfixStr"]; + else + $dest_record = $trigger_obj["prefixPostfixStr"] . $dest_record; + } } + + // Set redcap_event_name, record_id, and redcap_data_access_group if $import_dags is true + foreach ($dest_record_data as $redcap_event_name => $data) + { + $dest_record_data[$redcap_event_name][$dest_record_id] = $dest_record; + + if ($redcap_event_name != "classic") + { + $dest_record_data[$redcap_event_name]["redcap_event_name"] = $redcap_event_name; + } + + if ($import_dags) + { + $dest_record_data[$redcap_event_name]["redcap_data_access_group"] = $record_data[0]["redcap_data_access_group"]; + } + } + + $dest_record_data = array_values($dest_record_data); // Don't need the keys to push, only the values. } - } - else - { - $dest_record = $record_data[0][$link_source]; - } - - // Set record_id, and redcap_data_access_group if $import_dags is true - foreach ($dest_record_data as $i => $data) - { - $dest_record_data[$i][$dest_record_id] = $dest_record; - if ($import_dags) + + if (!empty($dest_record_data)) { - $dest_record_data[$i]["redcap_data_access_group"] = $record_data[0]["redcap_data_access_group"]; + global $Proj; + $SourceProj = $Proj; + + // Save DET data in destination project; + $save_response = REDCap::saveData($dest_project, "json", json_encode($dest_record_data), $overwrite_data); + + // HOTFIX: For issue where after saveData the global $Proj variable references the destination project context + if ($Proj->project_id !== $project_id) { + $Proj = $SourceProj; + } + + if (!empty($save_response["errors"])) + { + $this->log("DET Builder: Errors for Trigger #" . ($index + 1) . " Data not moved. Received the following: " . json_encode($save_response["errors"])); + } + + else if (!empty($save_response["warnings"])) + { + $this->log("DET Builder: Moved Data with Warnings for Trigger #" . ($index + 1) . ". Received the following: " . json_encode($save_response["warnings"])); + } + + else + { + $this->log("DET Builder: Moved Data for Trigger #" . ($index + 1) . ". Created/modified the following records in PID $dest_project: " . implode(", ", array_unique($save_response["ids"]))); + } + + /** + * If data was saved without errors then generate survey link and save it to the source project + **/ + + if (empty($save_response["errors"])) + { + $survey_url_event = $trigger_obj["surveyUrlEvent"]; + $survey_url_instrument = $trigger_obj["surveyUrl"]; + $save_url_event = $trigger_obj["saveUrlEvent"]; + $save_url_field = $trigger_obj["saveUrlField"]; + + if (!empty($survey_url_instrument) && !empty($save_url_field)) + { + if (!empty($survey_url_event)) + { + $RedcapProj = new Project($dest_project); + $dest_events = $RedcapProj->getUniqueEventNames(); + $survey_event_id = array_search($survey_url_event, $dest_events); + } + else + { + $survey_event_id = null; + } + + $survey_url = REDCap::getSurveyLink($dest_record, $survey_url_instrument, $survey_event_id, 1, $dest_project); + + if (is_null($survey_url)) + { + $this->log("DET Builder: Errors for Trigger #" . ($index + 1) . "Survey url couldn't be generated. Please check your parameters for REDCap::getSurveyLink(). Project = $dest_project, Record = $dest_record, Instrument = $survey_url_instrument, Event ID = " . (is_null($survey_event_id) ? "null" : $survey_event_id)); + } + else + { + $record_id_field = REDCap::getRecordIdField(); + + $save_url_data = [ + $record_id_field => $record, + $save_url_field => $survey_url, + "redcap_event_name" => empty($save_url_event) ? "" : $save_url_event + ]; + + $save_response = REDCap::saveData($project_id, "json", json_encode(array($save_url_data))); + + if (!empty($save_response["errors"])) + { + $this->log("DET Builder: Errors for Trigger #" . ($index + 1) . ". Unable to save survey url to $save_url_field. Received the following: " . json_encode($save_response["errors"])); + } + } + } + } } } - - $dest_record_data = array_values($dest_record_data); // Don't need the keys to push, only the values. - } - - if (!empty($dest_record_data)) - { - // Save DET data in destination project; - $save_response = REDCap::saveData($dest_project, "json", json_encode($dest_record_data), $overwrite_data); - - if (!empty($save_response["errors"])) - { - REDCap::logEvent("DET: Errors", json_encode($save_response["errors"]), null, $record, $event_id, $project_id); - } - else - { - REDCap::logEvent("DET: Ran successfully", "Data was successfully imported from project $project_id to project $dest_project", null, $record, $event_id, $project_id); - } - - if (!empty($save_response["warnings"])) - { - REDCap::logEvent("DET: Ran sucessfully with Warnings", json_encode($save_response["warnings"]), null, $record, $event_id, $project_id); - } - - if (!empty($save_response["ids"])) - { - REDCap::logEvent("DET: Modified/Saved the following records", json_encode($save_response["ids"]), null, null, null, $dest_project); - } } } } - + /** * Function to create and download release notes from settings in the DET Builder. */ - public function downloadReleaseNotes($settings) + public function downloadReleaseNotes($settings) { - $sourceProjectTitle = REDCap::getProjectTitle(); - - $query = $this->framework->createQuery(); - $query->add("select app_title from redcap_projects where project_id = ?", [$settings["dest-project"]]); - - if ($query_result = $query->execute()) + if ($this->getProjectSetting("enable-release-notes")) { - while($row = $query_result->fetch_assoc()) + $sourceProjectTitle = REDCap::getProjectTitle(); + + $query = $this->framework->createQuery(); + $query->add("select app_title from redcap_projects where project_id = ?", [$settings["dest-project"]]); + + if ($query_result = $query->execute()) { - $destProjectTitle = $row["app_title"]; + while($row = $query_result->fetch_assoc()) + { + $destProjectTitle = $row["app_title"]; + } } - } - - // Creating the new document... - $phpWord = new \PhpOffice\PhpWord\PhpWord(); - - $phpWord->getSettings()->setUpdateFields(true); // Forces document to update and set page numbers when first opened, as page numbers are missing from TOC, because of a bug. - - // Add styling - $phpWord->addFontStyle( - "generalFontStyle", - array('name' => 'Calibri', 'size' => 11, 'color' => 'black') - ); - $phpWord->addNumberingStyle( - 'hNum', - array('type' => 'multilevel', 'levels' => array( - array('pStyle' => 'Heading1', 'format' => 'decimal', 'text' => '%1'), - array('pStyle' => 'Heading2', 'format' => 'decimal', 'text' => '%1.%2'), - array('pStyle' => 'Heading3', 'format' => 'decimal', 'text' => '%1.%2.%3'), + + // Creating the new document... + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + + $phpWord->getSettings()->setUpdateFields(true); // Forces document to update and set page numbers when first opened, as page numbers are missing from TOC, because of a bug. + + // Add styling + $phpWord->addFontStyle( + "generalFontStyle", + array('name' => 'Calibri', 'size' => 11, 'color' => 'black') + ); + $phpWord->addNumberingStyle( + 'hNum', + array('type' => 'multilevel', 'levels' => array( + array('pStyle' => 'Heading1', 'format' => 'decimal', 'text' => '%1'), + array('pStyle' => 'Heading2', 'format' => 'decimal', 'text' => '%1.%2'), + array('pStyle' => 'Heading3', 'format' => 'decimal', 'text' => '%1.%2.%3'), ) - ) - ); - $phpWord->addTitleStyle( - 1, - array('name' => 'Calibri Light', 'size' => 18, 'color' => 'black', 'bold' => true), - array('numStyle' => 'hNum', 'numLevel' => 0) - ); - $phpWord->addFontStyle( - "titleFontStyle", - array('name' => 'Calibri Light', 'size' => 18, 'color' => 'black', 'bold' => true) - ); - $phpWord->addFontStyle( - "triggerFontStyle", - array('name' => 'Calibri', 'size' => 11, 'color' => 'black', 'bold' => true) - ); - $phpWord->addParagraphStyle( - "centerParagraphStyle", - array("align" => \PhpOffice\PhpWord\SimpleType\Jc::CENTER) - ); - $phpWord->addFontStyle( - "headerFontStyle", - array('name' => 'Times New Roman', 'size' => 18, 'color' => 'black', 'italic' => true) - ); - $phpWord->addParagraphStyle( - "rightParagraphStyle", - array("align" => \PhpOffice\PhpWord\SimpleType\Jc::END) - ); - $phpWord->addTableStyle( - "fieldInstrTableStyle", - array("width" => 100 * 50, "unit" => \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT, "borderSize" => 1, "borderColor" => 000000) - ); - $phpWord->addFontStyle( - "titleFontStyle", - array('name' => 'Calibri Light', 'size' => 18, 'color' => 'black', 'bold' => true, 'underline' => \PhpOffice\PhpWord\Style\Font::UNDERLINE_DASH) - ); - $phpWord->addFontStyle( - "boldFontStyle", - array('name' => 'Calibri Light', 'size' => 11, 'color' => 'black', 'bold' => true) - ); - $phpWord->addNumberingStyle( - 'multilevel', - array( - 'type' => 'multilevel', - 'levels' => array( - array('format' => 'decimal', 'text' => '%1.', 'left' => 360, 'hanging' => 360, 'tabPos' => 360), - array('format' => 'upperLetter', 'text' => '%2.', 'left' => 720, 'hanging' => 360, 'tabPos' => 720), ) - ) - ); - $lineStyle = array('weight' => 1, 'width' => 450, 'height' => 0, 'color' => 000000); - $cellStyle = array("bgColor" => 'D3D3D3'); - $tableStyle = array("width" => 100 * 50, "unit" => \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT, "borderSize" => 1, "borderColor" => 000000); - - /* Note: any element you append to a document must reside inside of a Section. */ - - // Title Page - $title_section = $phpWord->addSection(); - $header = $title_section->addHeader(); - $header->addText("Release Notes", "headerFontStyle", "rightParagraphStyle"); - $title_section->addText($sourceProjectTitle, "titleFontStyle", "centerParagraphStyle"); - $title_section->addText("Conducted by", "generalFontStyle", "centerParagraphStyle"); - $title_section->addText("Principal Investigator, Title, Affiliation", "generalFontStyle", "centerParagraphStyle"); - - $title_section->addTextBreak(); - $title_section->addText("Document History", "boldFontStyle", "centerParagraphStyle"); - - $doc_history_table = $title_section->addTable($tableStyle); - - $doc_history_table->addRow(); - $doc_history_table->addCell(1750)->addText("Version", array('bold' => true)); - $doc_history_table->addCell(1750)->addText("Changes Made", array('bold' => true)); - $doc_history_table->addCell(1750)->addText("Effective Date", array('bold' => true)); - - $doc_history_table->addRow(); - $doc_history_table->addCell(1750)->addText("1", "generalFontStyle"); - $doc_history_table->addCell(1750)->addText($this->getProjectSetting("saved_by"), "generalFontStyle"); - $doc_history_table->addCell(1750)->addText($this->getProjectSetting("saved_timestamp"), "generalFontStyle"); - - $title_section->addTextBreak(); - $title_section->addTextBreak(); - - $app_table = $title_section->addTable($tableStyle); - - $app_table->addRow(); - $app_table->addCell(1750)->addText("Application Initial Version", array('bold' => true)); - $app_table->addCell(1750)->addText(""); - - $app_table->addRow(); - $app_table->addCell(1750)->addText($sourceProjectTitle, "generalFontStyle"); - $app_table->addCell(1750)->addText("", "generalFontStyle"); - - $app_table->addRow(); - $app_table->addCell(1750)->addText("Project ID", "generalFontStyle"); - $app_table->addCell(1750)->addText($this->getProjectId(), "generalFontStyle"); - - $app_table->addRow(); - $app_table->addCell(1750)->addText($destProjectTitle, "generalFontStyle"); - $app_table->addCell(1750)->addText("", "generalFontStyle"); - - $app_table->addRow(); - $app_table->addCell(1750)->addText("Project ID", "generalFontStyle"); - $app_table->addCell(1750)->addText($settings["dest-project"], "generalFontStyle"); - - $app_table->addRow(); - $app_table->addCell(1750)->addText("Accessible", "generalFontStyle"); - $app_table->addCell(1750)->addText("World Wide Web", "generalFontStyle"); - - // Table of Contents - $toc_section = $phpWord->addSection(); - $header = $toc_section->addHeader(); - $header->addText("Release Notes", "headerFontStyle", "rightParagraphStyle"); - $toc_section->addText("Table of Contents", "titleFontStyle"); - $toc_section->addLine($lineStyle); - $toc_section->addTOC(array('name' => 'Calibri', 'size' => 11, 'color' => 'black')); - - $section = $phpWord->addSection(); - - // Add Header - $header = $section->addHeader(); - $header->addText("Release Notes", "headerFontStyle", "rightParagraphStyle"); - - // Purpose - $section->addTitle("Purpose", 1); - $section->addLine($lineStyle); - $section->addText("This document describes the '$sourceProjectTitle' Include a short description of the project and how it pertains to data management.", "generalFontStyle"); - // Scope - $section->addTitle("Scope", 1); - $section->addLine($lineStyle); - $section->addText("This document is to be used as a reference '$sourceProjectTitle' users for training purposes as well as for change requests tracking.", "generalFontStyle"); - - // Triggers - $section->addTitle("Database Set Up", 1); - $section->addLine($lineStyle); - - if ($settings["import-dags"]) - { - $section->addText("Create records in PID " . $settings["dest-project"] . " in the same data access groups (DAGs). DAG names will be the same across projects, though the IDs will be different."); - $section->addTextBreak(); - } - - foreach($settings["triggers"] as $index => $trigger) - { - $section->addText("Trigger #$index: Create a record in PID " . $settings["dest-project"] . ", and move data in table $index when the following condition is true:", "generalFontStyle"); - $section->addText(htmlspecialchars($trigger), "triggerFontStyle", "centerParagraphStyle"); - $section->addTextBreak(); - } - - // Fields/Instrument Linkage - $section->addTitle("Fields/Instrument Linkage", 1); - $section->addLine($lineStyle); - $section->addText("The data from the following variables in PID " . $this->getProjectId() . " are *copied* into PID " . $settings["dest-project"] . " automatically when conditions are met.", "generalFontStyle"); - $section->addTextBreak(); - - $text .= "Link records between source and destination project using "; - if (!empty($settings["linkSourceEvent"])) - { - $text .= "[" . $settings["linkSourceEvent"] . "]"; - } - $text .= "[" . $settings["linkSource"] . "] = "; - if (!empty($settings["linkDestEvent"])) - { - $text .= "[" . $settings["linkDestEvent"] . "]"; - } - $text .= "[" . $settings["linkDest"] . "]"; - $section->addText($text, "generalFontStyle"); - - foreach($settings["triggers"] as $index => $trigger) - { + ); + $phpWord->addTitleStyle( + 1, + array('name' => 'Calibri Light', 'size' => 18, 'color' => 'black', 'bold' => true), + array('numStyle' => 'hNum', 'numLevel' => 0) + ); + $phpWord->addFontStyle( + "titleFontStyle", + array('name' => 'Calibri Light', 'size' => 18, 'color' => 'black', 'bold' => true) + ); + $phpWord->addFontStyle( + "triggerFontStyle", + array('name' => 'Calibri', 'size' => 11, 'color' => 'black', 'bold' => true) + ); + $phpWord->addParagraphStyle( + "centerParagraphStyle", + array("align" => \PhpOffice\PhpWord\SimpleType\Jc::CENTER) + ); + $phpWord->addFontStyle( + "headerFontStyle", + array('name' => 'Times New Roman', 'size' => 18, 'color' => 'black', 'italic' => true) + ); + $phpWord->addParagraphStyle( + "rightParagraphStyle", + array("align" => \PhpOffice\PhpWord\SimpleType\Jc::END) + ); + $phpWord->addTableStyle( + "fieldInstrTableStyle", + array("width" => 100 * 50, "unit" => \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT, "borderSize" => 1, "borderColor" => 000000) + ); + $phpWord->addFontStyle( + "titleFontStyle", + array('name' => 'Calibri Light', 'size' => 18, 'color' => 'black', 'bold' => true, 'underline' => \PhpOffice\PhpWord\Style\Font::UNDERLINE_DASH) + ); + $phpWord->addFontStyle( + "boldFontStyle", + array('name' => 'Calibri Light', 'size' => 11, 'color' => 'black', 'bold' => true) + ); + $phpWord->addNumberingStyle( + 'multilevel', + array( + 'type' => 'multilevel', + 'levels' => array( + array('format' => 'decimal', 'text' => '%1.', 'left' => 360, 'hanging' => 360, 'tabPos' => 360), + array('format' => 'upperLetter', 'text' => '%2.', 'left' => 720, 'hanging' => 360, 'tabPos' => 720), + ) + ) + ); + $lineStyle = array('weight' => 1, 'width' => 450, 'height' => 0, 'color' => 000000); + $cellStyle = array("bgColor" => 'D3D3D3'); + $tableStyle = array("width" => 100 * 50, "unit" => \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT, "borderSize" => 1, "borderColor" => 000000); + + /* Note: any element you append to a document must reside inside of a Section. */ + + // Title Page + $title_section = $phpWord->addSection(); + $header = $title_section->addHeader(); + $header->addText("Release Notes", "headerFontStyle", "rightParagraphStyle"); + $title_section->addText($sourceProjectTitle, "titleFontStyle", "centerParagraphStyle"); + $title_section->addText("Conducted by", "generalFontStyle", "centerParagraphStyle"); + $title_section->addText("Principal Investigator, Title, Affiliation", "generalFontStyle", "centerParagraphStyle"); + + $title_section->addTextBreak(); + $title_section->addText("Document History", "boldFontStyle", "centerParagraphStyle"); + + $doc_history_table = $title_section->addTable($tableStyle); + + $doc_history_table->addRow(); + $doc_history_table->addCell(1750)->addText("Version", array('bold' => true)); + $doc_history_table->addCell(1750)->addText("Changes Made", array('bold' => true)); + $doc_history_table->addCell(1750)->addText("Effective Date", array('bold' => true)); + + $doc_history_table->addRow(); + $doc_history_table->addCell(1750)->addText("1", "generalFontStyle"); + $doc_history_table->addCell(1750)->addText($this->getProjectSetting("saved_by"), "generalFontStyle"); + $doc_history_table->addCell(1750)->addText($this->getProjectSetting("saved_timestamp"), "generalFontStyle"); + + $title_section->addTextBreak(); + $title_section->addTextBreak(); + + $app_table = $title_section->addTable($tableStyle); + + $app_table->addRow(); + $app_table->addCell(1750)->addText("Application Initial Version", array('bold' => true)); + $app_table->addCell(1750)->addText(""); + + $app_table->addRow(); + $app_table->addCell(1750)->addText($sourceProjectTitle, "generalFontStyle"); + $app_table->addCell(1750)->addText("", "generalFontStyle"); + + $app_table->addRow(); + $app_table->addCell(1750)->addText("Project ID", "generalFontStyle"); + $app_table->addCell(1750)->addText($this->getProjectId(), "generalFontStyle"); + + $app_table->addRow(); + $app_table->addCell(1750)->addText($destProjectTitle, "generalFontStyle"); + $app_table->addCell(1750)->addText("", "generalFontStyle"); + + $app_table->addRow(); + $app_table->addCell(1750)->addText("Project ID", "generalFontStyle"); + $app_table->addCell(1750)->addText($settings["dest-project"], "generalFontStyle"); + + $app_table->addRow(); + $app_table->addCell(1750)->addText("Accessible", "generalFontStyle"); + $app_table->addCell(1750)->addText("World Wide Web", "generalFontStyle"); + + // Table of Contents + $toc_section = $phpWord->addSection(); + $header = $toc_section->addHeader(); + $header->addText("Release Notes", "headerFontStyle", "rightParagraphStyle"); + $toc_section->addText("Table of Contents", "titleFontStyle"); + $toc_section->addLine($lineStyle); + $toc_section->addTOC(array('name' => 'Calibri', 'size' => 11, 'color' => 'black')); + + $section = $phpWord->addSection(); + + // Add Header + $header = $section->addHeader(); + $header->addText("Release Notes", "headerFontStyle", "rightParagraphStyle"); + + // Purpose + $section->addTitle("Purpose", 1); + $section->addLine($lineStyle); + $section->addText("This document describes the '$sourceProjectTitle' Include a short description of the project and how it pertains to data management.", "generalFontStyle"); + // Scope + $section->addTitle("Scope", 1); + $section->addLine($lineStyle); + $section->addText("This document is to be used as a reference '$sourceProjectTitle' users for training purposes as well as for change requests tracking.", "generalFontStyle"); + + // Triggers + $section->addTitle("Database Set Up", 1); + $section->addLine($lineStyle); + + if ($settings["import-dags"]) + { + $section->addText("Create records in PID " . $settings["dest-project"] . " in the same data access groups (DAGs). DAG names will be the same across projects, though the IDs will be different."); + $section->addTextBreak(); + } + + foreach($settings["triggers"] as $index => $trigger) + { + $section->addText("Trigger #$index: Create a record in PID " . $settings["dest-project"] . ", and move data in table $index when the following condition is true:", "generalFontStyle"); + $section->addText(htmlspecialchars($trigger), "triggerFontStyle", "centerParagraphStyle"); + $section->addTextBreak(); + } + + // Fields/Instrument Linkage + $section->addTitle("Fields/Instrument Linkage", 1); + $section->addLine($lineStyle); + $section->addText("The data from the following variables in PID " . $this->getProjectId() . " are *copied* into PID " . $settings["dest-project"] . " automatically when conditions are met.", "generalFontStyle"); $section->addTextBreak(); - $section->addText("Copy the following data into PID " . $settings["dest-project"] . " when trigger #$index is true", "generalFontStyle"); - - $fields_instr_table = $section->addTable($tableStyle); - // Table headers - $fields_instr_table->addRow(); - $fields_instr_table->addCell(1750, $cellStyle)->addText("From source project", array('bold' => true)); - $fields_instr_table->addCell(1750, $cellStyle)->addText("To destination project", array('bold' => true)); - - $pipingSourceEvents = $settings["pipingSourceEvents"][$index]; - $pipingDestEvents = $settings["pipingDestEvents"][$index]; - $pipingSourceFields = $settings["pipingSourceFields"][$index]; - $pipingDestFields = $settings["pipingDestFields"][$index]; - foreach($pipingSourceFields as $i => $source) + $text .= "Link records between source and destination project using "; + if (!empty($settings["linkSourceEvent"])) { - $text = ""; - $fields_instr_table->addRow(); - if (!empty($pipingSourceEvents[$i])) - { - $text .= "[" . $pipingSourceEvents[$i] . "]"; - } - $text .= "[" . $source . "]"; - $fields_instr_table->addCell(1750)->addText($text); - - $text = ""; - if (!empty($pipingDestEvents[$i])) - { - $text .= "[" . $pipingDestEvents[$i] . "]"; - } - $text .= "[" . $pipingDestFields[$i] . "]"; - $fields_instr_table->addCell(1750)->addText($text); + $text .= "[" . $settings["linkSourceEvent"] . "]"; } - - $setDestEvents = $settings["setDestEvents"][$index]; - $setDestFields = $settings["setDestFields"][$index]; - $setDestFieldsValues = $settings["setDestFieldsValues"][$index]; - foreach($setDestFields as $i => $source) + $text .= "[" . $settings["linkSource"] . "] = "; + if (!empty($settings["linkDestEvent"])) { - $text = ""; + $text .= "[" . $settings["linkDestEvent"] . "]"; + } + $text .= "[" . $settings["linkDest"] . "]"; + $section->addText($text, "generalFontStyle"); + + foreach($settings["triggers"] as $index => $trigger) + { + $section->addTextBreak(); + $section->addText("Copy the following data into PID " . $settings["dest-project"] . " when trigger #$index is true", "generalFontStyle"); + + $fields_instr_table = $section->addTable($tableStyle); + + // Table headers $fields_instr_table->addRow(); - if (!empty($setDestFieldsValues[$i])) + $fields_instr_table->addCell(1750, $cellStyle)->addText("From source project", array('bold' => true)); + $fields_instr_table->addCell(1750, $cellStyle)->addText("To destination project", array('bold' => true)); + + $pipingSourceEvents = $settings["pipingSourceEvents"][$index]; + $pipingDestEvents = $settings["pipingDestEvents"][$index]; + $pipingSourceFields = $settings["pipingSourceFields"][$index]; + $pipingDestFields = $settings["pipingDestFields"][$index]; + foreach($pipingSourceFields as $i => $source) { - $text .= "'" . $setDestFieldsValues[$i] . "'"; + $text = ""; + $fields_instr_table->addRow(); + if (!empty($pipingSourceEvents[$i])) + { + $text .= "[" . $pipingSourceEvents[$i] . "]"; + } + $text .= "[" . $source . "]"; + $fields_instr_table->addCell(1750)->addText($text); + + $text = ""; + if (!empty($pipingDestEvents[$i])) + { + $text .= "[" . $pipingDestEvents[$i] . "]"; + } + $text .= "[" . $pipingDestFields[$i] . "]"; $fields_instr_table->addCell(1750)->addText($text); } - - $text = ""; - if (!empty($setDestEvents[$i])) - { - $text .= "[" . $setDestEvents[$i] . "]"; - } - $text .= "[" . $source . "]"; - $fields_instr_table->addCell(1750)->addText($text); - } - - $sourceInstr = $settings["sourceInstr"][$index]; - $sourceInstrEvents = $settings["sourceInstrEvents"][$index]; - foreach($sourceInstr as $i => $source) - { - $text = ""; - $fields_instr_table->addRow(); - if (!empty($sourceInstrEvents[$i])) + + $setDestEvents = $settings["setDestEvents"][$index]; + $setDestFields = $settings["setDestFields"][$index]; + $setDestFieldsValues = $settings["setDestFieldsValues"][$index]; + foreach($setDestFields as $i => $source) { - $text .= "[" . $sourceInstrEvents[$i] . "]"; + $text = ""; + $fields_instr_table->addRow(); + if (!empty($setDestFieldsValues[$i])) + { + $text .= "'" . $setDestFieldsValues[$i] . "'"; + $fields_instr_table->addCell(1750)->addText($text); + } + + $text = ""; + if (!empty($setDestEvents[$i])) + { + $text .= "[" . $setDestEvents[$i] . "]"; + } + $text .= "[" . $source . "]"; + $fields_instr_table->addCell(1750)->addText($text); } - $text .= "[" . $source . "]"; - $fields_instr_table->addCell(1750)->addText($text); - - $text = ""; - if (!empty($sourceInstrEvents[$i])) + + $sourceInstr = $settings["sourceInstr"][$index]; + $sourceInstrEvents = $settings["sourceInstrEvents"][$index]; + foreach($sourceInstr as $i => $source) { - $text .= "[" . $sourceInstrEvents[$i] . "]"; + $text = ""; + $fields_instr_table->addRow(); + if (!empty($sourceInstrEvents[$i])) + { + $text .= "[" . $sourceInstrEvents[$i] . "]"; + } + $text .= "[" . $source . "]"; + $fields_instr_table->addCell(1750)->addText($text); + + $text = ""; + if (!empty($sourceInstrEvents[$i])) + { + $text .= "[" . $sourceInstrEvents[$i] . "]"; + } + $text .= "[" . $source . "]"; + $fields_instr_table->addCell(1750)->addText($text); } - $text .= "[" . $source . "]"; - $fields_instr_table->addCell(1750)->addText($text); } + + // Restrictions + $section->addTextBreak(); + $section->addTitle("Restrictions", 1); + $section->addLine($lineStyle); + $section->addText("Once the projects are in Production mode, the following action items should not occur in any of the projects under any circumstances. *DO NOT*: ", "generalFontStyle"); + $section->addTextBreak(); + + $section->addListItem('Rename/Update/Delete the *Record ID* field', 0, null, 'multilevel'); + $section->addListItem('Add test subjects', 0, null, 'multilevel'); + $section->addListItem('Create subjects manually in *All projects*', 0, null, 'multilevel'); + $section->addListItem('Edit the API user account rights (*api useraccount names*)', 0, null, 'multilevel'); + $section->addListItem('Change the field types (ex. text box fields to check box fields and vice versa)', 0, null, 'multilevel'); + $section->addListItem('Change/Rename the Field Name', 0, null, 'multilevel'); + $section->addListItem('Change/Edit the arm & event names ', 0, null, 'multilevel'); + + // Implementation And Approval + $section->addTextBreak(); + $section->addTitle("Implementation & Approval", 1); + $section->addLine($lineStyle); + + $completion_table = $section->addTable($tableStyle); + $completion_table->addRow(); + $completion_table->addCell(1750)->addText("Date Started", "boldFontStyle"); + $completion_table->addCell(1750); + $completion_table->addRow(); + $completion_table->addCell(1750)->addText("Date Completed", "boldFontStyle"); + $completion_table->addCell(1750); + $completion_table->addRow(); + $completion_table->addCell(1750)->addText("Total number of hours (DM Team)", "boldFontStyle"); + $completion_table->addCell(1750); + + $section->addTextBreak(); + + $dev_table = $section->addTable($tableStyle); + $dev_table->addRow(); + $dev_table->addCell(1750, $cellStyle)->addText("PROJECT LEADProject Request", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addRow(); + $dev_table->addCell(1750, $cellStyle)->addText("DEVELOPMENTProject Design and Setup.", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addRow(); + $dev_table->addCell(1750, $cellStyle)->addText("DEVELOPMENT", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addRow(); + $dev_table->addCell(1750, $cellStyle)->addText("DEV TESTING(ALPHA TESTING)", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addRow(); + $dev_table->addCell(1750*2, array("bgColor" => "D3D3D3", "gridSpan" => 4))->addText("BETA RELEASEDate:", "generalFontStyle", "centerParagraphStyle"); + $dev_table->addRow(); + $dev_table->addCell(1750, $cellStyle)->addText("BETA TESTING", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addRow(); + $dev_table->addCell(1750, $cellStyle)->addText("BCCH Research DMTeam Approval", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); + $dev_table->addCell(); + $dev_table->addRow(); + $dev_table->addCell(1750, array("bgColor" => "D3D3D3", "gridSpan" => 4))->addText("PROD RELEASEDate:", "generalFontStyle", "centerParagraphStyle"); + + // Enhancements & Approval + $section->addTextBreak(); + $section->addTitle("Enhancements & Approval", 1); + $section->addLine($lineStyle); + + $enhancement_table = $section->addTable($tableStyle); + $enhancement_table->addRow(); + $enhancement_table->addCell(1750, $cellStyle)->addText("Ticket ID#", "boldFontStyle"); + $enhancement_table->addCell(1750, $cellStyle)->addText("Description", "boldFontStyle"); + $enhancement_table->addCell(1750, $cellStyle)->addText("Status [Number of HW]", "boldFontStyle"); + $enhancement_table->addCell(1750, $cellStyle)->addText("Completed Date", "boldFontStyle"); + $enhancement_table->addRow(); + $enhancement_table->addCell(); + $enhancement_table->addCell(); + $enhancement_table->addCell(); + $enhancement_table->addCell(); + + // Bug Fixes + $section->addTextBreak(); + $section->addTitle("Bug Fixes", 1); + $section->addLine($lineStyle); + + $enhancement_table = $section->addTable($tableStyle); + $enhancement_table->addRow(); + $enhancement_table->addCell(1750, $cellStyle)->addText("JIRA ID", "boldFontStyle"); + $enhancement_table->addCell(1750, $cellStyle)->addText("Description", "boldFontStyle"); + $enhancement_table->addCell(1750, $cellStyle)->addText("Status", "boldFontStyle"); + $enhancement_table->addCell(1750, $cellStyle)->addText("Resolved Date", "boldFontStyle"); + $enhancement_table->addRow(); + $enhancement_table->addCell(); + $enhancement_table->addCell(); + $enhancement_table->addCell(); + $enhancement_table->addCell(); + + // Appendix: Key Terms + $section->addTextBreak(); + $section->addTitle("Appendix: Key terms", 1); + $section->addLine($lineStyle); + $section->addText("The following table provides definitions for terms relevant to this document.", "generalFontStyle"); + + $appendix_table = $section->addTable($tableStyle); + $appendix_table->addRow(); + $appendix_table->addCell(1750, $cellStyle)->addText("Term", "boldFontStyle"); + $appendix_table->addCell(1750, $cellStyle)->addText("Definition", "boldFontStyle"); + $appendix_table->addRow(); + $appendix_table->addCell()->addText("Alpha Testing", "generalFontStyle"); + $appendix_table->addCell()->addText("Alpha testing is simulated or actual operational testing by potential users/customers or an independent test team at the developers' site. Alpha testing is often employed for off-the-shelf software as a form of internal acceptance testing, before the software goes to beta testing.", "generalFontStyle"); + $appendix_table->addRow(); + $appendix_table->addCell()->addText("Beta Testing", "generalFontStyle"); + $appendix_table->addCell()->addText("Beta testing comes after alpha testing and can be considered a form of external user testing. Versions of the software, known as beta versions, are released to a limited audience outside of the programming team known as beta testers.", "generalFontStyle"); + $appendix_table->addRow(); + $appendix_table->addCell()->addText("DET", "generalFontStyle"); + $appendix_table->addCell()->addText("A Data Entry Trigger (DET) in REDCap is the capability to execute a script every time a survey or data entry form is saved.", "generalFontStyle"); + $appendix_table->addRow(); + $appendix_table->addCell()->addText("API", "generalFontStyle"); + $appendix_table->addCell()->addText('The acronym "API" stands for "Application Programming Interface". An API is just a defined way for a program to accomplish a task, usually retrieving or modifying data. API requests to REDCap are done using SSL (HTTPS), which means that the traffic to and from the REDCap server is encrypted. + ', "generalFontStyle"); + $appendix_table->addRow(); + $appendix_table->addCell()->addText("HW", "generalFontStyle"); + $appendix_table->addCell()->addText("Hours of Work.", "generalFontStyle"); + + // Support Information + $section->addTextBreak(); + $section->addTitle("Support Information", 1); + $section->addLine($lineStyle); + $section->addText("If you have any questions about this document or about the project, please contact at redcap@cfri.ca.", "generalFontStyle"); + + // Saving the document as OOXML file... + $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); + + // Stream file + header('Content-Description: File Transfer'); + header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessing'); + header('Content-Disposition: attachment; filename="release_notes.docx"'); + $objWriter->save("php://output"); } - - // Restrictions - $section->addTextBreak(); - $section->addTitle("Restrictions", 1); - $section->addLine($lineStyle); - $section->addText("Once the projects are in Production mode, the following action items should not occur in any of the projects under any circumstances. *DO NOT*: ", "generalFontStyle"); - $section->addTextBreak(); - - $section->addListItem('Rename/Update/Delete the *Record ID* field', 0, null, 'multilevel'); - $section->addListItem('Add test subjects', 0, null, 'multilevel'); - $section->addListItem('Create subjects manually in *All projects*', 0, null, 'multilevel'); - $section->addListItem('Edit the API user account rights (*api useraccount names*)', 0, null, 'multilevel'); - $section->addListItem('Change the field types (ex. text box fields to check box fields and vice versa)', 0, null, 'multilevel'); - $section->addListItem('Change/Rename the Field Name', 0, null, 'multilevel'); - $section->addListItem('Change/Edit the arm & event names ', 0, null, 'multilevel'); - - // Implementation And Approval - $section->addTextBreak(); - $section->addTitle("Implementation & Approval", 1); - $section->addLine($lineStyle); - - $completion_table = $section->addTable($tableStyle); - $completion_table->addRow(); - $completion_table->addCell(1750)->addText("Date Started", "boldFontStyle"); - $completion_table->addCell(1750); - $completion_table->addRow(); - $completion_table->addCell(1750)->addText("Date Completed", "boldFontStyle"); - $completion_table->addCell(1750); - $completion_table->addRow(); - $completion_table->addCell(1750)->addText("Total number of hours (DM Team)", "boldFontStyle"); - $completion_table->addCell(1750); - - $section->addTextBreak(); - - $dev_table = $section->addTable($tableStyle); - $dev_table->addRow(); - $dev_table->addCell(1750, $cellStyle)->addText("PROJECT LEADProject Request", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addRow(); - $dev_table->addCell(1750, $cellStyle)->addText("DEVELOPMENTProject Design and Setup.", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addRow(); - $dev_table->addCell(1750, $cellStyle)->addText("DEVELOPMENT", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addRow(); - $dev_table->addCell(1750, $cellStyle)->addText("DEV TESTING(ALPHA TESTING)", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addRow(); - $dev_table->addCell(1750*2, array("bgColor" => "D3D3D3", "gridSpan" => 4))->addText("BETA RELEASEDate:", "generalFontStyle", "centerParagraphStyle"); - $dev_table->addRow(); - $dev_table->addCell(1750, $cellStyle)->addText("BETA TESTING", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addRow(); - $dev_table->addCell(1750, $cellStyle)->addText("BCCH Research DMTeam Approval", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addCell(1750, $cellStyle)->addText("Date", "generalFontStyle"); - $dev_table->addCell(); - $dev_table->addRow(); - $dev_table->addCell(1750, array("bgColor" => "D3D3D3", "gridSpan" => 4))->addText("PROD RELEASEDate:", "generalFontStyle", "centerParagraphStyle"); - - // Enhancements & Approval - $section->addTextBreak(); - $section->addTitle("Enhancements & Approval", 1); - $section->addLine($lineStyle); - - $enhancement_table = $section->addTable($tableStyle); - $enhancement_table->addRow(); - $enhancement_table->addCell(1750, $cellStyle)->addText("Ticket ID#", "boldFontStyle"); - $enhancement_table->addCell(1750, $cellStyle)->addText("Description", "boldFontStyle"); - $enhancement_table->addCell(1750, $cellStyle)->addText("Status [Number of HW]", "boldFontStyle"); - $enhancement_table->addCell(1750, $cellStyle)->addText("Completed Date", "boldFontStyle"); - $enhancement_table->addRow(); - $enhancement_table->addCell(); - $enhancement_table->addCell(); - $enhancement_table->addCell(); - $enhancement_table->addCell(); - - // Bug Fixes - $section->addTextBreak(); - $section->addTitle("Bug Fixes", 1); - $section->addLine($lineStyle); - - $enhancement_table = $section->addTable($tableStyle); - $enhancement_table->addRow(); - $enhancement_table->addCell(1750, $cellStyle)->addText("JIRA ID", "boldFontStyle"); - $enhancement_table->addCell(1750, $cellStyle)->addText("Description", "boldFontStyle"); - $enhancement_table->addCell(1750, $cellStyle)->addText("Status", "boldFontStyle"); - $enhancement_table->addCell(1750, $cellStyle)->addText("Resolved Date", "boldFontStyle"); - $enhancement_table->addRow(); - $enhancement_table->addCell(); - $enhancement_table->addCell(); - $enhancement_table->addCell(); - $enhancement_table->addCell(); - - // Appendix: Key Terms - $section->addTextBreak(); - $section->addTitle("Appendix: Key terms", 1); - $section->addLine($lineStyle); - $section->addText("The following table provides definitions for terms relevant to this document.", "generalFontStyle"); - - $appendix_table = $section->addTable($tableStyle); - $appendix_table->addRow(); - $appendix_table->addCell(1750, $cellStyle)->addText("Term", "boldFontStyle"); - $appendix_table->addCell(1750, $cellStyle)->addText("Definition", "boldFontStyle"); - $appendix_table->addRow(); - $appendix_table->addCell()->addText("Alpha Testing", "generalFontStyle"); - $appendix_table->addCell()->addText("Alpha testing is simulated or actual operational testing by potential users/customers or an independent test team at the developers' site. Alpha testing is often employed for off-the-shelf software as a form of internal acceptance testing, before the software goes to beta testing.", "generalFontStyle"); - $appendix_table->addRow(); - $appendix_table->addCell()->addText("Beta Testing", "generalFontStyle"); - $appendix_table->addCell()->addText("Beta testing comes after alpha testing and can be considered a form of external user testing. Versions of the software, known as beta versions, are released to a limited audience outside of the programming team known as beta testers.", "generalFontStyle"); - $appendix_table->addRow(); - $appendix_table->addCell()->addText("DET", "generalFontStyle"); - $appendix_table->addCell()->addText("A Data Entry Trigger (DET) in REDCap is the capability to execute a script every time a survey or data entry form is saved.", "generalFontStyle"); - $appendix_table->addRow(); - $appendix_table->addCell()->addText("API", "generalFontStyle"); - $appendix_table->addCell()->addText('The acronym "API" stands for "Application Programming Interface". An API is just a defined way for a program to accomplish a task, usually retrieving or modifying data. API requests to REDCap are done using SSL (HTTPS), which means that the traffic to and from the REDCap server is encrypted. - ', "generalFontStyle"); - $appendix_table->addRow(); - $appendix_table->addCell()->addText("HW", "generalFontStyle"); - $appendix_table->addCell()->addText("Hours of Work.", "generalFontStyle"); - - // Support Information - $section->addTextBreak(); - $section->addTitle("Support Information", 1); - $section->addLine($lineStyle); - $section->addText("If you have any questions about this document or about the project, please contact at redcap@cfri.ca.", "generalFontStyle"); - - // Saving the document as OOXML file... - $filename = $this->getSystemSetting("temp-folder") . "/release_notes.docx"; - $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); - $objWriter->save($filename); - - // Stream file - header('Content-Description: File Transfer'); - header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessing'); - header('Content-Disposition: attachment; filename="'.basename($filename).'"'); - readfile($filename); - - // Delete file - unlink($filename); } - + /** * Function called by external module that checks whether the user has permissions to use the module. * Only returns the link if the user has admin privileges. - * + * * @param String $project_id Project ID of current REDCap project. * @param String $link Link that redirects to external module. - * @return NULL Return null if the user doesn't have permissions to use the module. - * @return String Return link to module if the user has permissions to use it. + * @return NULL Return null if the user doesn't have permissions to use the module. + * @return String Return link to module if the user has permissions to use it. */ public function redcap_module_link_check_display($project_id, $link) { diff --git a/README.md b/README.md index aa2f572..ba06f08 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,87 @@ -# Data Entry Trigger Builder Instructions +# Data Entry Trigger Builder -1. Make sure the module has been enabled in your project. After, navigate to External Modules, in your REDCap sidebar. Only administrators can see the link. +## Acknowledgement -![Step1](imgs/step1.jpg) +Some functionality within the module has been inspired by work done by Andy Martin (https://github.com/123andy), Jae Lee, and Ihab Zeedia at Stanford University. -2. Select your destination project. Your current project is automatically used as the source. If you’ve exported your DET settings from another project, you may import them, instead. +## Important -![Step2](imgs/step2.jpg) +This modules is an updated version of the module developed at BC Children's Hospital Research Institute. When switching from the previous module (https://github.com/BCCHR-IT/data-entry-trigger-builder), you'll have to enter your previous settings again, by hand. This is because the settings are stored as JSON in REDCap, and the structure of the JSON object has changed with ours. The module will not function otherwise. -3. Determine which fields you’ll use to link your source and destination projects. These fields will be used to link records between the projects, when at least one trigger condition is met. +## Instructions -![Step3](imgs/step3.jpg) +1. Make sure the module has been enabled in your project. After, navigate to External Modules, in your REDCap sidebar. Only administrators with "Access to all projects and data with maximum user privileges" can see the link. -4. Add a trigger to move data. Multiple conditions can be chained in one trigger using && (AND), or || (OR). See the table attached to the module, for allowed qualifiers. +![CustomApplicationLink](imgs/customApplicationLink.jpg) -![Step4](imgs/step4.jpg) +2. Add a trigger to move data. Multiple conditions can be chained in one trigger using && (AND), or || (OR). See the table attached to the module, for allowed qualifiers. If you’ve exported your DET settings from another project, you may import them, instead. + +![AddTrigger](imgs/addTrigger.png) + +The following form will appear. + +![AddTriggerForm](imgs/addTriggerForm.png) + +Start by creating a trigger. Data movement will occur when your trigger is met. + +![CreateTrigger](imgs/createTrigger.png) + +3. Select your destination project. Your current project is automatically used as the source. + +![SelectDestination](imgs/selectDestination.png) + +4. Determine which fields you’ll use to link your source and destination projects. These fields will be used to link records between the projects, when at least one trigger condition is met. + +![RecordLinkage](imgs/recordLinkage.png) + +(OPTIONAL) Decide whether you want to append a static prefix, or postfix to the linking value. + +![PrefixPostfix](imgs/addPrefixPostfix.png) 5. Add data to move when the condition is true. -![Step5](imgs/step5.jpg) +![DataMovement](imgs/dataMovement.png) + +Or create an empty record -5. - 1. Add an instrument to move. This can only work if there’s a one-to-one relationship between the selected instrument and another instrument in the destination project. Meaning all fields in the source instrument, must exist in the destination instrument. - 2. Add a field to move. You can choose to pipe a field, or manually set a value to move. (i.e set completion status to ‘2’) +![CreateEmptyRecord](imgs/createEmptyRecords.png) + +Data Movement Options: + +- Add an instrument to move. All fields in the chosen instrument must exist in the destination project, for the chosen event. +- Add a field to move. You can choose to pipe a field, or manually set a value to move. (i.e set completion status to ‘2’) -![Step5b](imgs/step5b.jpg) +![AddField](imgs/addField.jpg) -6. The module will allow up to ten triggers that each move their own data. +6. (OPTIONAL) Determine whether the module will pull the url for a survey into your source project, and where to save it. -![Step6](imgs/step6.jpg) +![GenerateSurveyUrl](imgs/generateSurveyURLs.png) 7. Determine whether you want blank fields to overwrite data in the destination project. -![Step7](imgs/step7.jpg) +![OverwriteData](imgs/overwriteData.jpg) 8. Determine whether you want to import DAGs to the destination project. This will only work if the DAGs are identical between projects. -![Step8](imgs/step8.jpg) +![ImportDAGs](imgs/importDAGs.jpg) + +9. The module will allow up to 10 triggers that each move their own data. -9. Save your DET. If it passes validation, then it will automatically run every time data is entered via a survey or data entry. Otherwise errors will be returned to you for correction. The DET will not save until your errors are corrected. +10. Save your DET. If it passes validation, then it will automatically run every time data is entered via a survey or data entry. Otherwise errors will be returned to you for correction. The DET will not save until your errors are corrected. -10. Once your DET is created, refresh the page, then you can export the settings as a JSON string. +11. Once your DET is created, refresh the page, then you can export the settings as a JSON string. -![Step9](imgs/step9.JPG) +![ExportSettings](imgs/exportSettings.JPG) -11. The module logs all DET activity. It will log successes, warnings, and errors. You may check there whenever you want to check on the status of your DET. +12. The module logs all DET errors. You may check there whenever you want to check on the status of your DET. # Warnings -1. Any changes made to the REDCap project, after the DET has been created, has the potential to break it. After you’ve updated your project, please make sure to update the DET in accordance with your changes. +- Any changes made to the REDCap project, after the DET has been created, has the potential to break it. After you’ve updated your project, please make sure to update the DET in accordance with your changes. +- Functionality developed at BCCHR to export your settings to a word document has been disabled, as it has not been made compatible with the new JSON structure. +- Module now logs to the external module logs, rather than a project's regular logs. # Limitations -1. Is not compatible with repeatable instruments. -2. Can only be used within the same instance of REDCap. -3. Can have a maximum of 10 triggers, with unlimited data to pipe. -4. Is not compatible with mult-arm projects at the moment. +- Is not compatible with repeatable instruments. +- Can have a maximum of 10 triggers, with unlimited data to pipe. \ No newline at end of file diff --git a/SubmitForm.php b/SubmitForm.php index d14ae41..16655c0 100644 --- a/SubmitForm.php +++ b/SubmitForm.php @@ -1,133 +1,134 @@ isValidEvent($settings["linkSourceEvent"])) +foreach($settings as $index => $trigger_obj) { - $errors["linkSourceEvent"] = "Invalid event!"; -} + $dest_project_pid = $trigger_obj["dest-project"]; -if (!$data_entry_trigger_builder->isValidField($settings["linkSource"])) -{ - $errors["linkSource"] = "Invalid field!"; -} + $err = $data_entry_trigger_builder->validateSyntax($trigger_obj["trigger"]); + if (!empty($err)) + { + $errors[$index]["trigger_errors"] = $err; + } -if (!empty($settings["linkDestEvent"]) && !$data_entry_trigger_builder->isValidEvent($settings["linkDestEvent"], $dest_project_pid)) -{ - $errors["linkDestEvent"] = "Invalid event!"; -} + if (!empty($trigger_obj["linkSourceEvent"]) && !$data_entry_trigger_builder->isValidEvent($trigger_obj["linkSourceEvent"])) + { + $errors[$index]["linkSourceEvent"] = "Invalid event!"; + } -if (!$data_entry_trigger_builder->isValidField($settings["linkDest"], $dest_project_pid)) -{ - $errors["linkDest"] = "Invalid field!"; -} + if (!$data_entry_trigger_builder->isValidField($trigger_obj["linkSource"])) + { + $errors[$index]["linkSource"] = "Invalid field!"; + } -foreach($settings["triggers"] as $index => $trigger) -{ - if (!empty($trigger)) + if (!empty($trigger_obj["linkDestEvent"]) && !$data_entry_trigger_builder->isValidEvent($trigger_obj["linkDestEvent"], $dest_project_pid)) { - $err = $data_entry_trigger_builder->validateSyntax($trigger); - if (!empty($err)) - { - $trigger_errors[$index] = $err; - } + $errors[$index]["linkDestEvent"] = "Invalid event!"; } -} -foreach($settings["pipingSourceEvents"] as $index => $fields) -{ - foreach($fields as $i => $field) + if (!$data_entry_trigger_builder->isValidField($trigger_obj["linkDest"], $dest_project_pid)) + { + $errors[$index]["linkDest"] = "Invalid field!"; + } + + foreach($trigger_obj["pipingSourceEvents"] as $n => $field) { if(!$data_entry_trigger_builder->isValidEvent($field)) { - $errors["pipingSourceEvents"][$index][$i] = "$field is an invalid event!"; + $errors[$index]["pipingSourceEvents"][$n] = "$field is an invalid event!"; } } -} -foreach($settings["pipingSourceFields"] as $index => $fields) -{ - foreach($fields as $i => $field) + foreach($trigger_obj["pipingSourceFields"] as $n => $field) { if(!$data_entry_trigger_builder->isValidField($field)) { - $errors["pipingSourceFields"][$index][$i] = "$field is an invalid field!"; + $errors[$index]["pipingSourceFields"][$n] = "$field is an invalid field!"; } } -} -foreach($settings["pipingDestEvents"] as $index => $fields) -{ - foreach($fields as $i => $field) + foreach($trigger_obj["pipingDestEvents"] as $n => $field) { if(!$data_entry_trigger_builder->isValidEvent($field, $dest_project_pid)) { - $errors["pipingDestEvents"][$index][$i] = "$field is an invalid event!"; + $errors[$index]["pipingDestEvents"][$n] = "$field is an invalid event!"; } } -} -foreach($settings["pipingDestFields"] as $index => $fields) -{ - foreach($fields as $i => $field) + foreach($trigger_obj["pipingDestFields"] as $n => $field) { if(!$data_entry_trigger_builder->isValidField($field, $dest_project_pid)) { - $errors["pipingDestFields"][$index][$i] = "$field is an invalid field!"; + $errors[$index]["pipingDestFields"][$n] = "$field is an invalid field!"; } } -} -foreach($settings["setDestEvents"] as $index => $fields) -{ - foreach($fields as $i => $field) + foreach($trigger_obj["setDestEvents"] as $n => $field) { if(!$data_entry_trigger_builder->isValidEvent($field, $dest_project_pid)) { - $errors["setDestEvents"][$index][$i] = "$field is an invalid event!"; + $errors[$index]["setDestEvents"][$n] = "$field is an invalid event!"; } } -} -foreach($settings["setDestFields"] as $index => $fields) -{ - foreach($fields as $i => $field) + foreach($trigger_obj["setDestFields"] as $n => $field) { if(!$data_entry_trigger_builder->isValidField($field, $dest_project_pid)) { - $errors["setDestFields"][$index][$i] = "$field is an invalid field!"; + $errors[$index]["setDestFields"][$n] = "$field is an invalid field!"; } } -} -foreach($settings["sourceInstrEvents"] as $index => $fields) -{ - foreach($fields as $i => $field) + foreach($trigger_obj["sourceInstrEvents"] as $n => $field) { - if(!$data_entry_trigger_builder->isValidEvent($field, $dest_project_pid)) + if(!$data_entry_trigger_builder->isValidEvent($field)) { - $errors["sourceInstrEvents"][$index][$i] = "$field is an invalid event!"; + $errors[$index]["sourceInstrEvents"][$n] = "$field is an invalid event!"; } } -} -foreach($settings["sourceInstr"] as $index => $fields) -{ - foreach($fields as $i => $field) + foreach($trigger_obj["sourceInstr"] as $n => $field) { - if(!$data_entry_trigger_builder->isValidInstrument($field, $dest_project_pid)) + if(!$data_entry_trigger_builder->isValidInstrument($field)) { - $errors["sourceInstr"][$index][$i] = "$field is an invalid instrument!"; + $errors[$index]["sourceInstr"][$n] = "$field is an invalid instrument!"; } } -} -if (!empty($trigger_errors)) -{ - $errors["trigger_errors"] = $trigger_errors; + foreach($trigger_obj["destInstrEvents"] as $n => $field) + { + if(!$data_entry_trigger_builder->isValidEvent($field, $dest_project_pid)) + { + $errors[$index]["destInstrEvents"][$n] = "$field is an invalid event!"; + } + } + + if (!empty($trigger_obj["surveyUrlEvent"]) && !$data_entry_trigger_builder->isValidEvent($trigger_obj["surveyUrlEvent"], $dest_project_pid)) + { + $errors[$index]["surveyUrlEvent"] = "Invalid event!"; + } + + if (!empty($trigger_obj["surveyUrl"]) && !$data_entry_trigger_builder->isValidInstrument($trigger_obj["surveyUrl"], $dest_project_pid)) + { + $errors[$index]["surveyUrl"] = "Invalid instrument!"; + } + + if (!empty($trigger_obj["saveUrlEvent"]) && !$data_entry_trigger_builder->isValidEvent($trigger_obj["saveUrlEvent"])) + { + $errors[$index]["saveUrlEvent"] = "Invalid event!"; + } + + if (!empty($trigger_obj["saveUrlField"]) && !$data_entry_trigger_builder->isValidField($trigger_obj["saveUrlField"])) + { + $errors[$index]["saveUrlField"] = "Invalid field!"; + } } if (!empty($errors)) diff --git a/composer.json b/composer.json index 3ff1518..af6f8d9 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,10 @@ { "require": { "phpoffice/phpword": "^0.18.1" + }, + "config": { + "platform": { + "php": "7.4.21" + } } } diff --git a/composer.lock b/composer.lock index 8c98384..6d50428 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5c3e22a514028ea4066940da51a3cde5", + "content-hash": "b08fd60cade2393aa1855885b6ccb0c3", "packages": [ { "name": "laminas/laminas-escaper", @@ -53,6 +53,14 @@ "escaper", "laminas" ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, "time": "2019-12-31T16:43:30+00:00" }, { @@ -101,20 +109,33 @@ "laminas", "zf" ], + "support": { + "forum": "https://discourse.laminas.dev/", + "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", + "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", + "source": "https://github.com/laminas/laminas-zendframework-bridge" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "abandoned": true, "time": "2020-09-14T14:23:00+00:00" }, { "name": "phpoffice/phpword", - "version": "0.18.2", + "version": "0.18.3", "source": { "type": "git", "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "aca10785cf68dc95d7f6fac4fe854979fef3f8db" + "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/aca10785cf68dc95d7f6fac4fe854979fef3f8db", - "reference": "aca10785cf68dc95d7f6fac4fe854979fef3f8db", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/be0190cd5d8f95b4be08d5853b107aa4e352759a", + "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a", "shasum": "" }, "require": { @@ -209,7 +230,11 @@ "word", "writer" ], - "time": "2021-06-04T20:58:45+00:00" + "support": { + "issues": "https://github.com/PHPOffice/PHPWord/issues", + "source": "https://github.com/PHPOffice/PHPWord/tree/0.18.3" + }, + "time": "2022-02-17T15:40:03+00:00" } ], "packages-dev": [], @@ -219,5 +244,9 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [] + "platform-dev": [], + "platform-overrides": { + "php": "7.2.5" + }, + "plugin-api-version": "2.3.0" } diff --git a/config.json b/config.json index 0d7b9f7..d9977c7 100644 --- a/config.json +++ b/config.json @@ -1,12 +1,12 @@ { - "name": "Data Entry Trigger Builder", + "name": "Data Entry Trigger Builder (UBC Version)", "namespace": "BCCHR\\DataEntryTriggerBuilder", - "description": "An External Module that provides an interface to create a DET between a source and destination project.", + "description": "An External Module that provides an interface to create a DET between a source and destination project. This version is forked from the original version developed by the BC Children's Hospital Research Institute, and has been updated and published, with their permission. It has significant changes, so please make sure to read the README carefully.", "authors": [ { "name": "Ashley Lee", - "email": "alee2@bcchr.ca", - "institution": "BC Children's Hospital Research Institute" + "email": "ashley.lee@ubc.ca", + "institution": "University of British Columbia Faculty of Medicine - Digital Solutions" } ], "links": { @@ -18,20 +18,9 @@ } ] }, - "system-settings": [ - { - "key": "temp-folder", - "name": "Directory path to folder to temporarily store release notes. *Don't add a slash at the end", - "required": true, - "type": "text", - "super-users-only": true - } - ], - "permissions": [ - "redcap_save_record" - ], - "framework-version": 3, + "framework-version": 14, "compatibility": { - "redcap-version-min": "8.0.1" + "redcap-version-min": "13.7.11", + "php-version-min": "8.2.0" } } \ No newline at end of file diff --git a/functions.js b/functions.js index c101ebd..30870f3 100644 --- a/functions.js +++ b/functions.js @@ -14,32 +14,32 @@ function createFieldRow() if ($('#field-value').val() != '') { var sourceField = "'" + $('#field-value').val() + "'"; - var sourceFieldElem = ""; + var sourceFieldElem = ""; if ($('#dest-event-select').val() && $('#dest-event-select').val() != '') { - var destEvent = '[' + $('#dest-event-select').val() + ']'; - var destEventElem = ""; + destEvent = '[' + $('#dest-event-select').val() + ']'; + destEventElem = ""; } - var destFieldElem = ""; + var destFieldElem = ""; var editFunction = 'fillFieldForm(this)' } else { if ($('#event-select').val() && $('#event-select').val() != '') { - var sourceEvent = '[' + $('#event-select').val() + ']'; - var sourceEventElem = ""; + sourceEvent = '[' + $('#event-select').val() + ']'; + sourceEventElem = ""; } var sourceField = '[' + $('#field-select').val() + ']'; - var sourceFieldElem = ""; + var sourceFieldElem = ""; if ($('#dest-event-select').val() && $('#dest-event-select').val() != '') { - var destEvent = '[' + $('#dest-event-select').val() + ']'; - var destEventElem = ""; + destEvent = '[' + $('#dest-event-select').val() + ']'; + destEventElem = ""; } - var destFieldElem = ""; + var destFieldElem = ""; var editFunction = 'fillPipingFieldForm(this)' } - var html = "" + + let html = "" + "" + sourceEvent + sourceField + sourceEventElem + sourceFieldElem + "" + "" + destEvent + destField + destEventElem + destFieldElem + "" + "" + @@ -57,15 +57,24 @@ function createInstrRow() var sourceEventElem = ''; if ($('#instr-event-select').val() && $('#instr-event-select').val() != '') { - var sourceEvent = '[' + $('#instr-event-select').val() + ']'; - var sourceEventElem = ""; + sourceEvent = '[' + $('#instr-event-select').val() + ']'; + sourceEventElem = ""; } var sourceInstr = '[' + $('#instr-select').val() + ']'; - var sourceInstrElem = ""; + var sourceInstrElem = ""; - var html = "" + + if ($('#dest-event-instrument').val() && $('#dest-event-instrument').val() != '') { + var destEvent = '[' + $('#dest-event-instrument').val() + ']'; + var destEventElem = ""; + } + else { + var destEvent = "Data is moving to a classic project, so there are no events"; + var destEventElem = ""; + } + + let html = "" + "" + sourceEvent + sourceInstr + sourceEventElem + sourceInstrElem + "" + - "" + sourceEvent + sourceInstr + "" + + "" + destEvent + destEventElem + "" + "" + "" + "" @@ -96,7 +105,7 @@ function updateTable(elem) } else { - var id = $(".table-id").val(); + let id = $(".table-id").val(); $("#" + id).find("tbody").append(newRow); } } @@ -108,53 +117,203 @@ function updateTable(elem) function addTrigger() { - var triggers = $(".trigger-and-data-wrapper"); - var trigNum = triggers.length; + let triggers = $(".trigger-and-data-wrapper"); + let trigNum = triggers.length; + let sourceIsLongitudinal = $("#det-form").attr("data-source-is-longitudinal"); - var html = "
" + + let html = "
" + "
" + "
" + "
" + - "
Trigger:
" + + "
Title
" + "
" + "
" + "
" + "" + "
" + "
" + - "" + + "" + + "
" + + "
" + + "
Trigger: #" + (triggers.length+1) + "
" + + "
" + + "
" + + "" + "
" + - "

" + - "Copy the following instruments/fields from source project to linked project when the above condition is true:" + - "

" + - " " + - "" + - "

" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "
From Source ProjectTo Linked ProjectEdit?Delete?
" + + "
Select a Linked Project
" + + "

The module will move the data into the chosen project.

" + + "
" + + "" + + "
" + + "
Record Linkage
" + + "

" + + "Create subjects/push data to linked project using variables in source and linked project. When the trigger is met, then records between the source and linked project will be linked via the chosen fields. An option to pick events will appear, if a project is longitudinal." + + " When linking projects with anything other than the record ID fields, 'Auto-numbering for records' must be turned on in the destination project." + + "

" + + "" + + "
Import Data Access Groups
" + + "

Import data access groups (DAGs) every time data is saved? The setting can only import DAGs if they have a one-to-one relationship with the destination project. If you want to conditionally move DAGs, then use the [redcap_data_access_group] field under Data Movement, and select 'No' below.

" + + "
" + + "
" + + "" + + "
" + + "" + + "
" + + "
" + + "
Data Movement
" + + "

Copy the data below from source project to linked project when the trigger is met.

" + + "Special Use Case" + + "

The [redcap_data_access_group] is a special field that can either transfer data access groups 1:1 between projects, or map to a regular REDCap variable (radiobutton or drop-down). If you want to manually set the DAG using a variable in the destination project, then you must use the unique group id of the DAG (example below).

" + + "

Note: Since the data access group applies to the entire record, it cannot be moved using 'Add Instrument'. If your destination project is longitudinal, then use the first even when tranferring DAGs.

" + + "Examples" + + "

To map a regular REDCap field: [redcap_data_access_group] = [dag_radio]

" + + "

To manually set the data access group: [redcap_data_access_group] = 51

" + + "Destination Project Data Access Groups" + + "

Use the table below to find a unique group id from your destination project, if you don't already know it.

" + + "

Please select a destination project, before you can see its data access groups.

" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "

" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
From Source ProjectTo Linked ProjectEdit?Delete?
" + + "
Generate Survey URLs (Optional)
" + + "

If specified, the destination project will generate a survey url for the participant to redirect to. If the trigger is met, a survey url will generate after the data is moved.

" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "
" + + "
" + + "
" + + "
" + + "" + + "
"; + + if (sourceIsLongitudinal == "yes") + { + html = html + "
" + + "" + + "
"; + } + + html = html + "
" + + "" + + "
" + + "
" + + "
" + + "
" + + "
Confirm the following
" + + "
" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "
" + + "
" + + "
" + + "
"; if (triggers.length == 0) { $("#trigger-instr").after(html); + $(".destination-project-select").selectpicker(); + } - else if (triggers.length < 10) + else if (triggers.length < 20) { triggers.last().after(html); + $(".destination-project-select").selectpicker(); } else { - alert("You have reached the maximum number of allowed triggers (10)") + alert("You have reached the maximum number of allowed triggers (20)") } } @@ -171,6 +330,7 @@ function clearInstrForm() { $('#instr-event-select').val(""); $('#instr-select').val(""); + $('#dest-event-instrument').val(""); } function fillPipingFieldForm(elem) @@ -187,7 +347,7 @@ function fillPipingFieldForm(elem) $('#event-select').val(row.find(".pipingSourceEvents").val()); } - if (row.find(".pipingDestEvents")) + if (row.find(".pipingDestEvents") && $('#dest-event-select').attr('data-is-longitudinal') == 'yes') { $('#dest-event-select').val(row.find(".pipingDestEvents").val()); } @@ -205,7 +365,7 @@ function fillFieldForm(elem) $('#field-value').val(row.find(".setDestFieldsValues").val()); $('#dest-field-select').val(row.find(".setDestFields").val()); - if (row.find(".setDestEvents")) + if (row.find(".setDestEvents") && $('#dest-event-select').attr('data-is-longitudinal') == 'yes') { $('#dest-event-select').val(row.find(".setDestEvents").val()); } @@ -225,6 +385,11 @@ function fillInstrForm(elem) $('#instr-event-select').val(row.find(".sourceInstrEvents").val()); } + if (row.find(".destInstrEvents") && $('#dest-event-instrument').attr('data-is-longitudinal') == 'yes') + { + $('#dest-event-instrument').val(row.find(".destInstrEvents").val()); + } + $('#add-instr-btn').text("Update"); $('#add-instr-modal').modal('show'); } @@ -244,49 +409,95 @@ function validateFieldForm() function validateInstrumentForm() { - if (($('#instr-event-select') && $('#instr-event-select').val() == '') || $('#instr-select').val() == '') + if (($('#instr-event-select') && $('#instr-event-select').val() == '') || + $('#instr-select').val() == '' || + ($('#dest-event-instrument').is(':visible') && $('#dest-event-instrument').val() == '')) { return false; } return true; } -function updateAutocompleteItems(data) +function updateElemAutocompleteItems(elem, metadata) { - var metadata = JSON.parse(data); + let isLongitudinal = metadata.isLongitudinal; + let isModal = elem.closest("add-instr-modal") || elem.closet("add-field-modal"); + destFields = metadata.fields; destEvents = metadata.events; - var isLongitudinal = metadata.isLongitudinal; + destInstruments = metadata.instruments; + // Update events if (isLongitudinal) { - $(".dest-events-autocomplete").autocomplete({source: destEvents}); - $(".dest-events-autocomplete").prop("required", true); - $(".dest-event-wrapper").show(); + if (isModal) { + elem.find(".dest-events-autocomplete").autocomplete({source: destEvents, appendTo: isModal.attr("id")}); + } + else { + elem.find(".dest-events-autocomplete").autocomplete({source: destEvents}); + } + elem.find(".dest-events-autocomplete").prop("required", true); + elem.find(".dest-event-wrapper").show(); + $("#add-instr-label-event-div").show(); + $("#dest-event-instrument, #dest-event-select").attr("data-is-longitudinal", "yes"); + } + else { + elem.find(".dest-events-autocomplete").val(""); + elem.find(".dest-events-autocomplete").prop("required", false); + elem.find(".dest-event-wrapper").hide(); + $("#add-instr-label-event-div").hide(); + $("#dest-event-instrument, #dest-event-select").attr("data-is-longitudinal", "no"); + } + + elem.find(".surveyUrlEvent").prop("required", false); // This field should always be optional + + // Update fields + if (isModal) { + elem.find(".dest-fields-autocomplete").autocomplete({source: destFields, appendTo: isModal.attr("id")}); } else { - $(".dest-events-autocomplete").val(""); - $(".dest-events-autocomplete").prop("required", false); - $(".dest-event-wrapper").hide(); + elem.find(".dest-fields-autocomplete").autocomplete({source: destFields}); } - $(".dest-fields-autocomplete").autocomplete({source: destFields}); + + elem.find(".surveyUrl").autocomplete({source: destInstruments}); } -function addError(id, error) +function addError(index, className, error) { - $('#' + id).addClass("error"); - $('#' + id).after("

" + error + "

"); + $('.' + className + ':eq(' + index + ')').addClass("error"); + $('.' + className + ':eq(' + index + ')').after("

" + error + "

"); } -function addTableErrors(errors, inputName) +function addTableErrors(index, errors, inputName) { - for(var index in errors) + let items = $("td > input[name='triggers[" + index + "][" + inputName + "][]']"); + for(let i in errors) { - var items = $("td > input[name='" + inputName + "[" + index + "][]']"); - console.log(items); - for(var i in errors[index]) + let msg = errors[i]; + $(items[i]).after("

" + msg + "

"); + } +} + +function updateDestDagTable(elem, groups) +{ + let selectProjectMsg = elem.find(".select-project-for-dags"); + let dagTable = elem.find(".dest-dag-table"); + let noDagsFound = elem.find(".no-dags-found"); + + selectProjectMsg.hide(); + + if (groups) + { + for(let i in groups) { - var msg = errors[index][i]; - $(items[i]).after("

" + msg + "

"); - } + let group = groups[i]; + dagTable.find("tbody").append("" + group + "" + "" + i + ""); + } + noDagsFound.hide(); + dagTable.show(); + } + else + { + noDagsFound.show(); + dagTable.hide(); } } \ No newline at end of file diff --git a/getDestinationFields.php b/getDestinationFields.php index 5aad657..f753101 100644 --- a/getDestinationFields.php +++ b/getDestinationFields.php @@ -1,4 +1,4 @@ retrieveProjectMetadata($_POST["pid"]); -print json_encode($fields); \ No newline at end of file +print json_encode($module->escape($fields)); \ No newline at end of file diff --git a/getDestinationInfo.php b/getDestinationInfo.php new file mode 100644 index 0000000..b862a73 --- /dev/null +++ b/getDestinationInfo.php @@ -0,0 +1,10 @@ +retrieveProjectMetadata($pid); +$groups = $data_entry_trigger_builder->retrieveProjectGroups($pid); +$to_return = [ + "metadata" => $fields, + "groups" => $groups +]; +print json_encode($module->escape($to_return)); \ No newline at end of file diff --git a/imgs/step5b.jpg b/imgs/addField.jpg similarity index 100% rename from imgs/step5b.jpg rename to imgs/addField.jpg diff --git a/imgs/addPrefixPostfix.png b/imgs/addPrefixPostfix.png new file mode 100644 index 0000000..43973d0 Binary files /dev/null and b/imgs/addPrefixPostfix.png differ diff --git a/imgs/addTrigger.png b/imgs/addTrigger.png new file mode 100644 index 0000000..6be912b Binary files /dev/null and b/imgs/addTrigger.png differ diff --git a/imgs/addTriggerForm.png b/imgs/addTriggerForm.png new file mode 100644 index 0000000..8c8370f Binary files /dev/null and b/imgs/addTriggerForm.png differ diff --git a/imgs/createEmptyRecords.png b/imgs/createEmptyRecords.png new file mode 100644 index 0000000..8f0c3bb Binary files /dev/null and b/imgs/createEmptyRecords.png differ diff --git a/imgs/createTrigger.png b/imgs/createTrigger.png new file mode 100644 index 0000000..b5e865b Binary files /dev/null and b/imgs/createTrigger.png differ diff --git a/imgs/step1.jpg b/imgs/customApplicationLink.jpg similarity index 100% rename from imgs/step1.jpg rename to imgs/customApplicationLink.jpg diff --git a/imgs/dataMovement.png b/imgs/dataMovement.png new file mode 100644 index 0000000..2e46c2a Binary files /dev/null and b/imgs/dataMovement.png differ diff --git a/imgs/step9.JPG b/imgs/exportSettings.JPG similarity index 100% rename from imgs/step9.JPG rename to imgs/exportSettings.JPG diff --git a/imgs/generateSurveyURLs.png b/imgs/generateSurveyURLs.png new file mode 100644 index 0000000..ee827bd Binary files /dev/null and b/imgs/generateSurveyURLs.png differ diff --git a/imgs/step8.jpg b/imgs/importDAGs.jpg similarity index 100% rename from imgs/step8.jpg rename to imgs/importDAGs.jpg diff --git a/imgs/step7.jpg b/imgs/overwriteData.jpg similarity index 100% rename from imgs/step7.jpg rename to imgs/overwriteData.jpg diff --git a/imgs/recordLinkage.png b/imgs/recordLinkage.png new file mode 100644 index 0000000..7397585 Binary files /dev/null and b/imgs/recordLinkage.png differ diff --git a/imgs/selectDestination.png b/imgs/selectDestination.png new file mode 100644 index 0000000..930e278 Binary files /dev/null and b/imgs/selectDestination.png differ diff --git a/imgs/step2.jpg b/imgs/step2.jpg deleted file mode 100644 index 63d653d..0000000 Binary files a/imgs/step2.jpg and /dev/null differ diff --git a/imgs/step3.jpg b/imgs/step3.jpg deleted file mode 100644 index bfae8b5..0000000 Binary files a/imgs/step3.jpg and /dev/null differ diff --git a/imgs/step4.jpg b/imgs/step4.jpg deleted file mode 100644 index 71b9c95..0000000 Binary files a/imgs/step4.jpg and /dev/null differ diff --git a/imgs/step5.jpg b/imgs/step5.jpg deleted file mode 100644 index be989ee..0000000 Binary files a/imgs/step5.jpg and /dev/null differ diff --git a/imgs/step6.jpg b/imgs/step6.jpg deleted file mode 100644 index facb11a..0000000 Binary files a/imgs/step6.jpg and /dev/null differ diff --git a/index.php b/index.php index 4cd2fbc..c1fda24 100644 --- a/index.php +++ b/index.php @@ -1,39 +1,84 @@ getProjectSetting("det_settings"), true); - $dest_fields = $data_entry_trigger_builder->retrieveProjectMetadata($settings["dest-project"]); + $settings = decode_json($data_entry_trigger_builder->getProjectSetting("det_settings")); } -$Proj = new Project(); ?> - - + + + project['status'] > 0 || !empty($settings)): ?> -
+
project['status'] > 0): ?>
This project is currently in production, be careful with your changes!
WARNING: Any changes made to the REDCap project, after the DET has been created, has the potential to break it. After you’ve updated your project, please make sure to update the DET in accordance with your changes.
- project['status'] > 0) { - // print "
The destination project is currently in production.
"; - // } - // } - ?>

Data Entry Trigger Builder

-

LIMITATIONS*: This module will work will classical and longitudinal projects, but is currently incompatible with repeatable events, and multiple arms.

+

LIMITATIONS*: This module will work will classical and longitudinal projects, but is currently incompatible with repeatable events and instruments.

DET was last changed on getProjectSetting("saved_timestamp");?> by getProjectSetting("saved_by");?>


Import/Export Your DET Settings

- If you've created a JSON string containing your DET settings, you may import them into the module, or you may export your current DET settings (If they exist). - When importing settings for projects on a different REDCap instance that have the same structure, change the destination project id before import. + If you've created a JSON string containing your DET settings, you may import them into the module, or you may export your current DET settings (If they exist). When importing settings for projects on a different REDCap instance that have the same structure, change the destination project id before import.

IMPORTANT: Once you've imported your DET settings, you must still save them by clicking "Save DET" at the bottom of the page.

@@ -107,82 +158,18 @@


- + getProjectSetting("enable-release-notes")): ?>
Download Release Notes
" method="post">

-
-
Select a Linked Project
-
- - project['status'] > 0) { - print "

This project is currently in production.

"; - } - } - ?> -
-
-
style="display:none" > -
Record Linkage
-

- Create subjects/push data to linked project using variables in source and linked project. - When at least one of the triggers are met, then records between the source and linked project will be linked via the chosen fields. -

-

IMPORTANT: When linking projects with anything other than the record ID fields, "Auto-numbering for records" must be turned on in the destination project.

- -
-
Triggers (Max. 10)
+ "> +
+
Triggers (Max. 20)
- +
  • E.g., [event_name][instrument_name_complete] = "2"
  • E.g., [event_name][variable_name] = "1"
  • @@ -244,31 +231,176 @@ -
- $trigger): ?> + $trigger_obj): ?>
- +
- + +
+
+ +
+
+ +
+
Select a Linked Project
+

The module will move the data into the chosen project.

+
+ + project['status'] > 0) { + print "

This project is currently in production.

"; + } + } + ?> +
+
Record Linkage
+

+ Create subjects/push data to linked project using variables in source and linked project. When the trigger is met, then records between the source and linked project will be linked via the chosen fields. An option to pick events will appear, if a project is longitudinal. When linking projects with anything other than the record ID fields, 'Auto-numbering for records' must be turned on in the destination project. +

+ +
Import Data Access Groups
+

+ Import data access groups (DAGs) every time data is saved? The setting can only import DAGs if they have a one-to-one relationship with the destination project. If you want to conditionally move DAGs, then use the [redcap_data_access_group] field under Data Movement, and select 'No' below. +

+
+
+ + +
+ + + +
+ + +
+
Data Movement
+

Copy the following instruments/fields from source project to linked project when the trigger is met.

+ Special Use Case

- Copy the following instruments/fields from source project to linked project when the above condition is true: + The [redcap_data_access_group] is a special field that can either transfer data access groups 1:1 between projects, or map to a regular REDCap variable (radiobutton or drop-down). If you want to manually set the DAG using a variable in the destination project, then you must use the unique group id of the DAG (example below).

- - +

Note: Since the data access group applies to the entire record, it cannot be moved using 'Add Instrument'. If your destination project is longitudinal, then use the first even when tranferring DAGs.

+ Examples +

To map a regular REDCap field: [redcap_data_access_group] = [dag_radio]

+

To manually set the data access group: [redcap_data_access_group] = 51

+ Destination Project Data Access Groups +

Use the table below to find a unique group id from your destination project, if you don't already know it.

+

There are no data access groups in the destination project.

+ + + + + + + + +
Unique Group NameUnique Group ID
+ +

" class="table"> @@ -281,39 +413,41 @@ $source) { $pipingSourceEvent = htmlspecialchars($pipingSourceEvents[$i], ENT_QUOTES); $source = htmlspecialchars($source, ENT_QUOTES); + $pipingDestEvent = htmlspecialchars($pipingDestEvents[$i], ENT_QUOTES); + $dest = htmlspecialchars($pipingDestFields[$i], ENT_QUOTES); print ""; print ""; print ""; } - $setDestEvents = $settings["setDestEvents"][$index]; - $setDestFields = $settings["setDestFields"][$index]; - $setDestFieldsValues = $settings["setDestFieldsValues"][$index]; + $setDestEvents = $trigger_obj["setDestEvents"]; + $setDestFields = $trigger_obj["setDestFields"]; + $setDestFieldsValues = $trigger_obj["setDestFieldsValues"]; foreach($setDestFields as $i => $source) { @@ -322,44 +456,50 @@ $source = htmlspecialchars($source, ENT_QUOTES); print ""; print ""; print ""; } - $sourceInstr = $settings["sourceInstr"][$index]; - $sourceInstrEvents = $settings["sourceInstrEvents"][$index]; + $sourceInstr = $trigger_obj["sourceInstr"]; + $sourceInstrEvents = $trigger_obj["sourceInstrEvents"]; + $destInstrEvents = $trigger_obj["destInstrEvents"]; foreach($sourceInstr as $i => $source) { $sourceInstrEvent = htmlspecialchars($sourceInstrEvents[$i], ENT_QUOTES); $source = htmlspecialchars($source, ENT_QUOTES); + $destInstrEvent = htmlspecialchars($destInstrEvents[$i], ENT_QUOTES); print ""; print ""; print ""; @@ -367,48 +507,54 @@ ?>
"; if (!empty($pipingSourceEvent)) { print "[" . $pipingSourceEvent . "]"; - print ""; + print ""; } print "[" . $source . "]"; - print ""; + print ""; if (!empty($pipingDestEvents[$i])) { - print "[" . $pipingSourceEvent . "]"; - print ""; + print "[" . $pipingDestEvent . "]"; + print ""; } - print "[" . $pipingSourceEvent . "]"; - print ""; + print "[" . $dest . "]"; + print ""; print "
"; - if (!empty($setDestFieldsValue)) + if ($setDestFieldsValue !== "") { print "'" . $setDestFieldsValue . "'"; - print ""; + print ""; } if (!empty($setDestEvent)) { print "[" . $setDestEvent . "]"; - print ""; + print ""; } print "[" . $source . "]"; - print ""; + print ""; print "
"; if (!empty($sourceInstrEvent)) { print "[" . $sourceInstrEvent . "]"; - print ""; + print ""; } print "[" . $source . "]"; - print ""; - if (!empty($sourceInstrEvent)) + print ""; + if (!empty($destInstrEvent)) { - print "[" . $sourceInstrEvent . "]"; + print "[" . $destInstrEvent . "]"; + print ""; + } + else + { + print "Data is moving to a classic project, so there are no events"; } - print "[" . $source . "]"; print "
-
- -
-
Confirm the following
-
-
-
- - -
- - - - -
- - - -
- - - +
Generate Survey URLs (Optional)
+

If specified, the destination project will generate a survey url for the participant to redirect to. If the trigger is met, a survey url will generate after the data is moved.

+
+
+ +
+
> + " placeholder="Type to search for event"> +
+
+ " placeholder="Type to search for instrument"> +
+
+
+
+ +
+ +
+ " placeholder="Type to search for event"> +
+ +
+ " placeholder="Type to search for field"> +
+
+
-
-
- - -
- - - - -
- - - -
- - - +
Confirm the following
+
+
+ +
+ + +
+ + + +
+ + +
+
+ +
@@ -447,10 +593,10 @@
-
- +
+
-
+
@@ -474,7 +620,10 @@
+
+
+
+
+
+
+ +
+
-
+
diff --git a/script.php b/script.php index 09a8978..fad142e 100644 --- a/script.php +++ b/script.php @@ -3,225 +3,286 @@ $instrument_names = REDCap::getInstrumentNames(); ?> \ No newline at end of file