Skip to content

Commit

Permalink
[Candidate_parameters] Improve front-end validation for entering Cons…
Browse files Browse the repository at this point in the history
…ent data (#4034)

This makes entering consent data more rigorous. Implements the following workflow for data entry:

- Date of consent being given is required for both answers to consent "yes" or "no"
- Date of withdrawal of consent required if answer changes from "yes" to "no"
- Consent status "yes" or "no" cannot be changed to an empty one
- Replaces confirmation message to swal
- Adds script to check errors in data
  • Loading branch information
zaliqarosli authored and driusan committed Feb 11, 2019
1 parent 846a979 commit 9ef6e11
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 70 deletions.
76 changes: 68 additions & 8 deletions modules/candidate_parameters/ajax/formHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,19 +398,20 @@ function editParticipantStatusFields($db, $user)
function editConsentStatusFields($db, $user)
{
if (!$user->hasPermission('candidate_parameter_edit')) {
header("HTTP/1.1 403 Forbidden");
header('HTTP/1.1 403 Forbidden');
exit;
}

// get CandID
// Get CandID
$candIDParam = $_POST['candID'];
$candID = (isset($candIDParam) && $candIDParam !== "null") ?
$candID = (isset($candIDParam) && $candIDParam !== 'null') ?
$candIDParam : null;

$candidate = \Candidate::singleton($candID);
$currentUser = \User::singleton();
$uid = $currentUser->getUsername();
// get pscid

// Get PSCID
$pscid = $candidate->getPSCID();

// Get list of all consent types
Expand All @@ -423,6 +424,7 @@ function editConsentStatusFields($db, $user)
$consentLabel = $consent['Label'];

// Process posted data
// Empty strings and type null are not passed (null is passed as a string)
$status = ($_POST[$consentName] !== 'null') ?
$_POST[$consentName] : null;
$date = ($_POST[$consentName . '_date'] !== 'null') ?
Expand All @@ -446,10 +448,69 @@ function editConsentStatusFields($db, $user)
'DateWithdrawn' => $withdrawal,
'EntryStaff' => $uid,
];
$recordExists = array_key_exists($consentID, $candidateConsent);
$emptyResult = empty($status) && empty($date) && empty($withdrawal);

if (!$emptyResult) {
// Validate data
$recordExists = array_key_exists($consentID, $candidateConsent);
$oldStatus = $candidateConsent[$consentID]['Status'] ?? null;
$validated = false;

switch ($status) {
case 'yes':
// Giving "yes" status requires consent date and empty withdrawal date
if (!empty($date) && empty($withdrawal)) {
$validated = true;
} else {
http_response_code(400);
echo('Data failed validation. Resolve errors and try again.');
return;
}
break;
case 'no':
// Giving 'no' status requires consent date and empty withdrawal date if
// record does not already exist
if (!$recordExists) {
if (!empty($date) && empty($withdrawal)) {
$validated = true;
} else {
http_response_code(400);
echo('Answering no to a consent type for the first time
requires only the date of consent.');
return;
}
} else { // If no status stays no or record existed as NULL,
// consent date and empty withdrawal date still required
if (($oldStatus === null || $oldStatus === 'no') && !empty($date)
&& empty($withdrawal)
) {
$validated = true;
} else if ($oldStatus === 'yes' && !empty($date)
&& !empty($withdrawal)
) { // Withdrawing from 'yes' status required consent date
// and withdrawal date
$validated = true;
} else {
http_response_code(400);
echo('Data failed validation. Resolve errors and try again.');
return;
}
}
break;
default:
// If status is empty, and date fields are also empty,
// validated is still false
// If status is empty but at least one of the date fields
// are filled, throw an error
if (!empty($date) || !empty($withdrawal)) {
http_response_code(400);
echo('A status is missing for at least one consent type.
Please select a valid status for all consent types.');
return;
}
break;
}

// Submit data
if ($validated) {
if ($recordExists) {
$db->update(
'candidate_consent_rel',
Expand All @@ -464,6 +525,5 @@ function editConsentStatusFields($db, $user)
}
$db->insert('candidate_consent_history', $updateHistory);
}

}
}
2 changes: 1 addition & 1 deletion modules/candidate_parameters/js/CandidateParameters.js

Large diffs are not rendered by default.

121 changes: 60 additions & 61 deletions modules/candidate_parameters/jsx/ConsentStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ class ConsentStatus extends React.Component {
},
Data: [],
formData: {},
updateResult: null,
errorMessage: null,
error: false,
isLoaded: false,
loadedData: 0
};
Expand All @@ -29,7 +28,6 @@ class ConsentStatus extends React.Component {
this.fetchData = this.fetchData.bind(this);
this.setFormData = this.setFormData.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.showAlertMessage = this.showAlertMessage.bind(this);
}

componentDidMount() {
Expand Down Expand Up @@ -66,8 +64,9 @@ class ConsentStatus extends React.Component {
});
},
error: error => {
console.error(error);
this.setState({
error: 'An error occurred when loading the form!'
error: true
});
}
});
Expand All @@ -82,6 +81,21 @@ class ConsentStatus extends React.Component {
setFormData(formElement, value) {
let formData = this.state.formData;
formData[formElement] = value;
for (let consent in this.state.Data.consents) {
if (this.state.Data.consents.hasOwnProperty(consent)) {
const oldConsent = this.state.Data.consentStatuses[consent];
const newConsent = this.state.formData[consent];
// Clear withdrawal date if consent status changes from no
// (or empty if uncleaned data) to yes
if (formElement === consent) {
if ((newConsent === "yes" && oldConsent !== "yes") ||
(newConsent === "no" && oldConsent === null)) {
formData[consent + "_withdrawal"] = '';
formData[consent + "_withdrawal2"] = '';
}
}
}
}
this.setState({
formData: formData
});
Expand Down Expand Up @@ -148,6 +162,7 @@ class ConsentStatus extends React.Component {
// Set form data
let formData = new FormData();
for (let key in myFormData) {
// Does not submit data with empty string
if (myFormData[key] !== "") {
formData.append(key, myFormData[key]);
}
Expand All @@ -162,44 +177,24 @@ class ConsentStatus extends React.Component {
contentType: false,
processData: false,
success: data => {
this.setState({
updateResult: "success"
});
this.showAlertMessage();
swal('Success!', 'Update successful.', 'success');
this.fetchData();
},
error: error => {
if (error.responseText !== "") {
let errorMessage = JSON.parse(error.responseText).message;
this.setState({
updateResult: "error",
errorMessage: errorMessage
});
this.showAlertMessage();
}
console.error(error);
let errorMessage = error.responseText || 'Update failed.';
swal('Error!', errorMessage, 'error');
}
});
}

/**
* Display a success/error alert message after form submission
*/
showAlertMessage() {
if (this.refs["alert-message"] === null) {
return;
render() {
// If error occurs, return a message.
// XXX: Replace this with a UI component for 500 errors.
if (this.state.error) {
return <h3>An error occured while loading the page.</h3>;
}

let alertMsg = this.refs["alert-message"];
$(alertMsg).fadeTo(2000, 500).delay(3000).slideUp(
500,
() => {
this.setState({
updateResult: null
});
});
}

render() {
if (!this.state.isLoaded) {
return <Loader />;
}
Expand All @@ -210,20 +205,39 @@ class ConsentStatus extends React.Component {
disabled = false;
updateButton = <ButtonElement label ="Update" />;
}
let dateRequired = [];
let withdrawalRequired = [];
const emptyOption = [];
const dateRequired = [];
const withdrawalRequired = [];
const withdrawalDisabled = [];
let i = 0;
for (let consent in this.state.Data.consents) {
if (this.state.Data.consents.hasOwnProperty(consent)) {
let withdrawal = consent + "_withdrawal";

if (this.state.formData[consent] === "yes") {
const oldConsent = this.state.Data.consentStatuses[consent];
const newConsent = this.state.formData[consent];
const withdrawalDate = this.state.Data.withdrawals[consent];
// Set defaults
emptyOption[i] = true;
dateRequired[i] = false;
withdrawalRequired[i] = false;
// Let date of withdrawal field be disabled until it is needed
withdrawalDisabled[i] = true;
// If answer to consent is "yes", require date of consent
if (newConsent === "yes") {
dateRequired[i] = true;
}
if (this.state.formData[withdrawal]) {
withdrawalRequired[i] = true;
} else {
withdrawalRequired[i] = false;
// If answer to consent is "no", require date of consent
if (newConsent === "no") {
dateRequired[i] = true;
// If answer was previously "yes" and consent is now being withdrawn, enable and require withdrawal date
// If consent was previously withdrawn and stays withdrawn, enable and require withdrawal date
if (oldConsent === "yes" || (oldConsent === "no" && withdrawalDate)) {
withdrawalDisabled[i] = false;
withdrawalRequired[i] = true;
}
}
// Disallow clearing a valid consent status by removing empty option
if (oldConsent === "no" || oldConsent === "yes") {
emptyOption[i] = false;
}
i++;
}
Expand Down Expand Up @@ -255,6 +269,7 @@ class ConsentStatus extends React.Component {
ref={consentStatus}
disabled={disabled}
required={false}
emptyOption={emptyOption[i]}
/>
<DateElement
label={consentDateLabel}
Expand All @@ -280,16 +295,16 @@ class ConsentStatus extends React.Component {
value={this.state.formData[consentWithdrawal]}
onUserInput={this.setFormData}
ref={consentWithdrawal}
disabled={disabled}
required={false}
disabled={disabled || withdrawalDisabled[i]}
required={withdrawalRequired[i]}
/>
<DateElement
label={consentWithdrawalConfirmationLabel}
name={consentWithdrawal2}
value={this.state.formData[consentWithdrawal2]}
onUserInput={this.setFormData}
ref={consentWithdrawal2}
disabled={disabled}
disabled={disabled || withdrawalDisabled[i]}
required={withdrawalRequired[i]}
/>
<hr/>
Expand Down Expand Up @@ -349,24 +364,8 @@ class ConsentStatus extends React.Component {
}
}

let alertMessage = "";
let alertClass = "alert text-center hide";
if (this.state.updateResult) {
if (this.state.updateResult === "success") {
alertClass = "alert alert-success text-center";
alertMessage = "Update Successful!";
} else if (this.state.updateResult === "error") {
let errorMessage = this.state.errorMessage;
alertClass = "alert alert-danger text-center";
alertMessage = errorMessage ? errorMessage : "Failed to update!";
}
}

return (
<div className="row">
<div className={alertClass} role="alert" ref="alert-message">
{alertMessage}
</div>
<FormElement
name="consentStatus"
onSubmit={this.handleSubmit}
Expand Down
52 changes: 52 additions & 0 deletions tools/single_use/Cleanup_Consent_Data.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
/**
* This script is written for a one time use only to clean up existing data in the 'candidate_consent_rel' table to conform to the new front-end validation for entering consent.
*
* PHP Version 7
*
* @category Main
* @package Loris
* @author Zaliqa Rosli <zaliqa.rosli@mcin.ca>
* @licence Loris license
* @link https://github.com/aces/Loris
*/
require_once __DIR__ . '/../generic_includes.php';

$db = \Database::singleton();
$errors = array();

$query = "SELECT CandidateID, ConsentID, Status, DateGiven, DateWithdrawn
FROM candidate_consent_rel";
$consentData = $db->pselect($query, array());

foreach($consentData as $key => $entry) {
$candID = $entry['CandidateID'];
$consentID = $entry['ConsentID'];
$status = $entry['Status'];
$consentDate = $entry['DateGiven'];
$withdrawalDate = $entry['DateWithdrawn'];
$row = $key + 1;

switch ($status) {
case '':
array_push($errors, "An entry with a NULL consent status should not exist. Entry $row with CandidateID=$candID and ConsentID=$consentID has a NULL consent status. Please clear the entry or change the consent to either 'yes' or 'no'. \n\n");
break;
case 'no':
if (empty($consentDate)) {
array_push($errors, "A consent date is now required for both a 'yes' and 'no' consent status. 'DateGiven' is missing for entry $row with CandidateID=$candID and ConsentID=$consentID. Please fill in missing date of consent or clear the entry. \n\n");
}
break;
case 'yes':
if (empty($consentDate)) {
array_push($errors, "A consent date is now required for both a 'yes' and 'no' answer to consent. The 'DateGiven' column is missing for entry $row with CandidateID=$candID and ConsentID=$consentID. Please fill in missing date of consent or clear the entry. \n\n");
} else if (!empty($withdrawalDate)) {
array_push($errors, "An entry with a consent status 'yes' should not have a date of withdrawal. 'DateWithdrawn' exists for entry $row with CandidateID=$candID and ConsentID=$consentID. Please remove date of withdrawal or change the consent to 'no'. \n\n");
}
break;
}
}
if (empty($errors)) {
echo "There are no errors. No additional steps required.\n";
} else {
print_r($errors);
}

0 comments on commit 9ef6e11

Please sign in to comment.