From cb6420e23ffa28aa9101d953feb11ed96e45daf8 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Tue, 26 Nov 2024 16:27:03 -0500 Subject: [PATCH 1/6] LEAF exper server side get progress --- LEAF-Automated-Tests | 2 +- LEAF_Request_Portal/sources/Form.php | 340 ++++++++++++++------------- 2 files changed, 172 insertions(+), 170 deletions(-) diff --git a/LEAF-Automated-Tests b/LEAF-Automated-Tests index a7f6435a6..8d87883ef 160000 --- a/LEAF-Automated-Tests +++ b/LEAF-Automated-Tests @@ -1 +1 @@ -Subproject commit a7f6435a65ae82b646ebfd70cd3ea92015b96a38 +Subproject commit 8d87883efbef6bf4e028868c2db41ec42c2d0a35 diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 4945a1f5d..9eaf580b2 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -1509,206 +1509,208 @@ public function doSubmit(int $recordID): array return $return_value; } -/** + /** * Get the progress percentage (as integer), accounting for conditinally hidden questions * @param int $recordID * @return int Percent completed */ public function getProgress($recordID) { - $returnValue = 0; - - $vars = array(':recordID' => (int)$recordID); - //get all the catIDs associated with this record and whether the forms are enabled or submitted - $resRecordInfoEachForm = $this->db->prepared_query('SELECT recordID, categoryID, `count`, submitted FROM records - LEFT JOIN category_count USING (recordID) - WHERE recordID=:recordID', $vars); - $isSubmitted = false; + $subSQL = 'SELECT submitted FROM records + LEFT JOIN category_count USING (recordID) + WHERE recordID=:recordID'; + $subVars = array(':recordID' => (int)$recordID); + //First check if submitted and just return 100 if it is. + $resRecordInfoEachForm = $this->db->prepared_query($subSQL, $subVars); foreach ($resRecordInfoEachForm as $request) { if ($request['submitted'] > 0) { - $isSubmitted = true; - break; + return 100; } } - if ($isSubmitted) { - $returnValue = 100; - } else { - //use recordInfo about forms associated with the recordID to get the total number of required questions on the request - $allRequiredIndicators = $this->db->prepared_query("SELECT categoryID, indicatorID, `format`, conditions FROM indicators WHERE required=1 AND disabled = 0", array()); - $categories = array(); - foreach($resRecordInfoEachForm as $form) { - if((int)$form['count'] === 1) { - $categories[] = $form['categoryID']; - } - } - $resRequestRequired = array(); - foreach ($allRequiredIndicators as $indicator) { - if(in_array($indicator['categoryID'], $categories)) { - $resRequestRequired[] = $indicator; - } - } - $countRequestRequired = count($resRequestRequired); - //if there are no required questions we are done - if($countRequestRequired === 0) { - $returnValue = 100; + $dataSQL = 'SELECT indicatorID, `format`, `data` FROM `data` + LEFT JOIN indicators USING (indicatorID) + WHERE recordID=:recordID + AND indicators.disabled = 0 + AND `data` != ""'; + $dataVars = array(':recordID' => (int)$recordID); + $resData = $this->db->prepared_query($dataSQL, $dataVars); - } else { - //get indicatorID, format, data, and required for all completed questions for non-disabled indicators - $resCompleted = $this->db->prepared_query('SELECT indicatorID, `format`, `data`, `required` FROM `data` LEFT JOIN indicators - USING (indicatorID) - WHERE recordID=:recordID - AND indicators.disabled = 0 - AND data != "" - AND data IS NOT NULL', $vars); - - //used to count the number of required completions and organize completed data for possible condition checks - $resCountCompletedRequired = 0; - $resCompletedIndIDs = array(); - - foreach ($resCompleted as $entry) { - $arrData = @unserialize($entry['data']) === false ? array($entry['data']) : unserialize($entry['data']); - $entryFormat = trim(explode(PHP_EOL, $entry['format'])[0]); - - /*Edgecases: Checkbox and checkboxes format entries save the value 'no' if their corresponding input checkbox is unchecked. - Verifying that entries for these formats do not consist solely of 'no' values prevents a miscount that can occur if - they are later updated to be required, since not having empty values is no longer confirmation that they are answered.*/ - $checkBoxesAllNo = strtolower($entryFormat) === 'checkbox' || strtolower($entryFormat) === 'checkboxes'; - if ($checkBoxesAllNo === true) { - foreach ($arrData as $ele) { - if ($ele !== 'no') { - $checkBoxesAllNo = false; - break; - } - } - } - /*Grid formats are likewise not necessarily answered if they are not empty strings. Unanswered grid cells are empty */ - $gridContainsEmptyValue = false; - if (strtolower($entryFormat) === 'grid' && isset($arrData['cells'])) { - $arrRowEntries = $arrData['cells']; - foreach ($arrRowEntries as $row) { - if (in_array("", $row)) { - $gridContainsEmptyValue = true; - break; - } - } - } - if ($checkBoxesAllNo === false && $gridContainsEmptyValue === false) { - if ((int) $entry['required'] === 1) { - $resCountCompletedRequired++; - if ($resCountCompletedRequired === $countRequestRequired) { - break; - } - } - //save the data entry (whether required or not) in case it is needed for condition checking - $resCompletedIndIDs[(int) $entry['indicatorID']] = $entry['data']; - } - } - - if ($resCountCompletedRequired === $countRequestRequired) { - $returnValue = 100; + $dataTable = array(); + foreach($resData as $d) { + $dataTable[$d['indicatorID']] = $d['data']; + } + $returnValue = 0; + $requiredTotal = 0; + $requiredAnswered = 0; - } else { - //finally, check if there are conditions that result in required questions being in a hidden state, and adjust count and percentage + function count_required(array $dataTable, array $formNode, bool $parentOrSelfHidden = false, int &$required_total, int &$required_answered) + { + //don't care about any of this if the question is in a hidden state + if($parentOrSelfHidden === false) { + //Check for conditions and if the state is hidden. + $format = trim(strtolower($formNode['format'])); + if(!empty($formNode['conditions']) && $formNode['conditions'] !== 'null') { + $conditions = json_decode(strip_tags($formNode['conditions'])); $multiChoiceParentFormats = array('multiselect', 'checkboxes'); $singleChoiceParentFormats = array('radio', 'dropdown', 'number', 'currency'); + $conditionMet = false; + foreach ($conditions as $c) { + if ($c->childFormat === $format && + (strtolower($c->selectedOutcome) === 'hide' || strtolower($c->selectedOutcome) === 'show')) { + + $parentFormat = $c->parentFormat; + $conditionParentValue = preg_split('/\R/', $c->selectedParentValue) ?? []; + $currentParentDataValue = preg_replace('/'/', ''', $dataTable[$c->parentIndID] ?? ''); + if ($parentFormat === 'checkbox') { //single checkbox ifthen is either checked or not checked + $currentParentDataValue = !empty($currentParentDataValue) && $currentParentDataValue !== 'no' ? '1' : '0'; + } + if (in_array($parentFormat, $multiChoiceParentFormats)) { + $currentParentDataValue = @unserialize($currentParentDataValue) === false ? + array($currentParentDataValue) : unserialize($currentParentDataValue); + } else { + $currentParentDataValue = array($currentParentDataValue); + } - foreach ($resRequestRequired as $ind) { - //if question is not complete, and there are conditions (conditions could potentially have the string null due to a past import issue) ... - if (!in_array((int) $ind['indicatorID'], array_keys($resCompletedIndIDs)) && !empty($ind['conditions']) && $ind['conditions'] !== 'null') { - - $conditions = json_decode(strip_tags($ind['conditions'])); - $currFormat = preg_split('/\R/', $ind['format'])[0] ?? ''; - - $conditionMet = false; - foreach ($conditions as $c) { - if ($c->childFormat === $currFormat //if current format and condition format match - && (strtolower($c->selectedOutcome) === 'hide' || strtolower($c->selectedOutcome) === 'show')) { //and outcome is hide or show - - $parentFormat = $c->parentFormat; - $conditionParentValue = preg_split('/\R/', $c->selectedParentValue) ?? []; - //if parent data does not exist, use an empty string, since potential 'not' comparisons would need this. - $currentParentDataValue = preg_replace('/'/', ''', $resCompletedIndIDs[(int) $c->parentIndID] ?? ''); - + $operator = $c->selectedOp; + switch ($operator) { + case '==': + case '!=': if (in_array($parentFormat, $multiChoiceParentFormats)) { - $currentParentDataValue = @unserialize($currentParentDataValue) === false ? array($currentParentDataValue) : unserialize($currentParentDataValue); - } else { - $currentParentDataValue = array($currentParentDataValue); - } - - $operator = $c->selectedOp; - switch ($operator) { - case '==': - case '!=': - if (in_array($parentFormat, $multiChoiceParentFormats)) { - //true if the current data value includes any of the condition values - foreach ($currentParentDataValue as $v) { - if (in_array($v, $conditionParentValue)) { - $conditionMet = true; - break; - } - } - } else if (in_array($parentFormat, $singleChoiceParentFormats) && $currentParentDataValue[0] === $conditionParentValue[0]) { + //true if the current data value includes any of the condition values + foreach ($currentParentDataValue as $v) { + if (in_array($v, $conditionParentValue)) { $conditionMet = true; + break; } - if($operator === "!=") { - $conditionMet = !$conditionMet; - } - break; - case 'gt': - case 'gte': - case 'lt': - case 'lte': - $arrNumVals = array(); - $arrNumComp = array(); - foreach($currentParentDataValue as $v) { - if(is_numeric($v)) { - $arrNumVals[] = (float) $v; - } - } - foreach($conditionParentValue as $cval) { - if(is_numeric($cval)) { - $arrNumComp[] = (float) $cval; - } + } + } else if (in_array($parentFormat, $singleChoiceParentFormats) && $currentParentDataValue[0] === $conditionParentValue[0]) { + $conditionMet = true; + } + if($operator === "!=") { + $conditionMet = !$conditionMet; + } + break; + case 'gt': + case 'gte': + case 'lt': + case 'lte': + $arrNumVals = array(); + $arrNumComp = array(); + foreach($currentParentDataValue as $v) { + if(is_numeric($v)) { + $arrNumVals[] = (float) $v; + } + } + foreach($conditionParentValue as $cval) { + if(is_numeric($cval)) { + $arrNumComp[] = (float) $cval; + } + } + $useOrEqual = str_contains($operator, 'e'); + $useGreaterThan = str_contains($operator, 'g'); + $lenValues = count(array_values($arrNumVals)); + $lenCompare = count(array_values($arrNumComp)); + if($lenCompare > 0) { + for ($i = 0; $i < $lenValues; $i++) { + $currVal = $arrNumVals[$i]; + if($useGreaterThan === true) { + $conditionMet = $useOrEqual === true ? $currVal >= max($arrNumComp) : $currVal > max($arrNumComp); + } else { + $conditionMet = $useOrEqual === true ? $currVal <= min($arrNumComp) : $currVal < min($arrNumComp); } - $useOrEqual = str_contains($operator, 'e'); - $useGreaterThan = str_contains($operator, 'g'); - $lenValues = count(array_values($arrNumVals)); - $lenCompare = count(array_values($arrNumComp)); - if($lenCompare > 0) { - for ($i = 0; $i < $lenValues; $i++) { - $currVal = $arrNumVals[$i]; - if($useGreaterThan === true) { - $conditionMet = $useOrEqual === true ? $currVal >= max($arrNumComp) : $currVal > max($arrNumComp); - } else { - $conditionMet = $useOrEqual === true ? $currVal <= min($arrNumComp) : $currVal < min($arrNumComp); - } - if($conditionMet === true) { - break; - } - } + if($conditionMet === true) { + break; } + } + } + break; + default: + break; + } + } + //if in hidden state, set parenthidden to true and break out of condition checking. + if (($conditionMet === false && strtolower($c->selectedOutcome) === 'show') || + ($conditionMet === true && strtolower($c->selectedOutcome) === 'hide')) { + $parentOrSelfHidden = true; + break; + } + } + } + + //if still not in hidden state and required: increment required total. Check for answer and increment answered total if answered. + if(!$parentOrSelfHidden && (int)$formNode['required'] === 1) { + $required_total += 1; + $answered = false; + $val = $formNode['value']; + if(isset($val) && $val !== '') { + /*value property is already processed based on format and could be string or array. + Convert to array for more consistent comparison and to simplify checkbox(es) and grid formats.*/ + $valType = gettype($val); + + $arrVal = array(); + if($valType === 'array') { + $arrVal = $val; + } elseif ($valType === 'string') { + $val = trim($val); + $arrVal = @unserialize($val) === false ? array($val) : unserialize($val); + } + //these formats can have values despite being unanswered ('no', or serialized data about the entry) + $specialFormat = $format === 'checkbox' || $format === 'checkboxes' || $format === 'grid'; + if ($specialFormat === true) { + if($format === 'grid') { + $hasInput = isset($arrVal['cells']); + $gridContainsEmptyValue = !$hasInput; + if($hasInput) { + $arrRowEntries = $arrVal['cells']; + foreach ($arrRowEntries as $row) { + if (in_array("", $row)) { + $gridContainsEmptyValue = true; break; - default: - break; + } } } - //if the question is not being shown due to its conditions, do not count it as a required question - if (($conditionMet === false && strtolower($c->selectedOutcome) === 'show') || - ($conditionMet === true && strtolower($c->selectedOutcome) === 'hide')) { - $countRequestRequired--; - break; + $answered = !$gridContainsEmptyValue; + + } else { + //checkbox(es) - only one needed + foreach($arrVal as $ele) { + if ($ele !== '' && $ele !== 'no') { + $answered = true; + break; + } } } + + //any other format, confirm the element is not an empty string + } else { + $answered = $arrVal[0] !== ''; } } - if ($countRequestRequired === 0) { - $returnValue = 100; - } else { - $returnValue = round(100 * ($resCountCompletedRequired / $countRequestRequired)); + + if($answered === true) { + $required_answered += 1; } } } + //progress tree depth. + if(isset($formNode['child'])) { + foreach($formNode['child'] as $child) { + count_required($dataTable, $child, $parentOrSelfHidden, $required_total, $required_answered); + } + } + } + + $fullForm = $this->getFullForm($recordID); + foreach($fullForm as $page) { + count_required($dataTable, $page, false, $requiredTotal, $requiredAnswered); + } + + error_log("R A: ".$requiredTotal." ".$requiredAnswered); + if ($requiredTotal === 0) { + $returnValue = 100; + } else { + $returnValue = round(100 * ($requiredAnswered / $requiredTotal)); } return $returnValue; } From 3643446f8267e5c2c19acb7fc68d989deedeb8a0 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Tue, 26 Nov 2024 17:10:04 -0500 Subject: [PATCH 2/6] LEAF exper, formjs side validation, remove error_log --- LEAF_Request_Portal/js/form.js | 115 ++++++++++++++++++--------- LEAF_Request_Portal/sources/Form.php | 1 - 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/LEAF_Request_Portal/js/form.js b/LEAF_Request_Portal/js/form.js index 2da387910..5d37d4604 100644 --- a/LEAF_Request_Portal/js/form.js +++ b/LEAF_Request_Portal/js/form.js @@ -227,20 +227,28 @@ var LeafForm = function (containerID) { /** cross walk end */ let childRequiredValidators = {}; + //store required validators for the controlled question and any subchildren on main entry and modals const handleChildValidators = (childID) => { - if (!childRequiredValidators[childID]) { - childRequiredValidators[childID] = { - validator: formRequired[`id${childID}`]?.setRequired, - }; - } - //reset the validator, if there is one, from the stored value - if ( - childRequiredValidators[childID].validator !== undefined && - dialog !== null - ) { - dialog.requirements[childID] = - childRequiredValidators[childID].validator; - } + let arrValidatorIDs = [ childID ]; + const arrSubchildren = Array.from( + document.querySelectorAll(`.response.blockIndicator_${childID} div.response[class*="blockIndicator_"]`) + ); + arrSubchildren.forEach(element => { + const id = +element.className.match(/(\d+)$/)?.[0]; + if(id > 0) { + arrValidatorIDs.push(id); + } + }); + arrValidatorIDs.forEach(id => { + if (!childRequiredValidators[id]) { + childRequiredValidators[id] = { + validator: formRequired[`id${id}`]?.setRequired, + }; + } + if (childRequiredValidators[id].validator !== undefined && dialog !== null) { + dialog.requirements[id] = childRequiredValidators[id].validator; + } + }); }; //validator ref for required question in a hidden state const hideShowValidator = function () { @@ -361,32 +369,61 @@ var LeafForm = function (containerID) { return sanitize(val).trim(); }; - /* clear out potential entries and set validator for hidden questions */ - const clearValues = (childFormat = "", childIndID = 0) => { - $("#" + childIndID).val(""); //clears most formats - $(`input[id^="${childIndID}_"]`).prop("checked", false); //radio and checkbox(es) formats - $(`input[id^="${childIndID}_radio0"]`).prop("checked", true); - - $(`#grid_${childIndID}_1_input tbody td`) //grid table data - .each(function () { - if ($("textarea", this).length) { - $("textarea", this).val(''); - } else if ($("select", this).length) { - $("select", this).val(''); - } else if ($("input", this).length) { - $("input", this).val(''); + /*hide the question and any subquestions. clear out potential entries and set validator for hidden questions */ + const clearValues = (childIndID = 0) => { + const arrSubchildren = Array.from( + document.querySelectorAll(`.response.blockIndicator_${childIndID} div.response[class*="blockIndicator_"]`) + ); + + //parse the IDs of any additional subquestions + let arrChildAndSubquestionIDs = [ childIndID ]; + arrSubchildren.forEach(element => { + const id = +element.className.match(/(\d+)$/)?.[0]; + if(id > 0) { + arrChildAndSubquestionIDs.push(id); } }); - if (childFormat === "multiselect") { - clearMultiSelectChild($("#" + childIndID), childIndID); - } - if ( - childRequiredValidators[childIndID].validator !== undefined && - dialog !== null - ) { - dialog.requirements[childIndID] = hideShowValidator; - } + arrChildAndSubquestionIDs.forEach(id => { + //clear values for questions in a hidden state. + $("#" + id).val(""); //most formats + $(`input[id^="${id}_"]`).prop("checked", false); //radio and checkbox(es) formats + + $(`#grid_${id}_1_input tbody td`) //grid table data + .each(function () { + if ($("textarea", this).length) { + $("textarea", this).val(''); + } else if ($("select", this).length) { + $("select", this).val(''); + } else if ($("input", this).length) { + $("input", this).val(''); + } + }); + + const isMultiselectQuestion = document.querySelector(`select[id="${id}"][multiple]`) !== null; + if (isMultiselectQuestion) { + clearMultiSelectChild($("#" + id), id); + } + + const isRadioQuestion = document.querySelector(`input[id^="${id}_radio"]`) !== null; + if(isRadioQuestion) { + const radioEmpty = $(`input[id^="${id}_radio0"]`); //need to add hidden empty input to clear radio + if (radioEmpty.length === 0) { + $(`div.response.blockIndicator_${id}`).prepend( + `` + ); + } + $(`input[id^="${id}_radio0"]`).prop("checked", true); + } + + //if the question is required, use the alternate validator + if ( + childRequiredValidators[id].validator !== undefined && + dialog !== null + ) { + dialog.requirements[id] = hideShowValidator; + } + }); }; /** @@ -526,7 +563,7 @@ var LeafForm = function (containerID) { switch (co) { case "hide": if (hideShowConditionMet === true) { - clearValues(childFormat, childID); + clearValues(childID); elChildResponse.classList.add('response-hidden'); elsChild.hide(); elsChild.attr('aria-hidden', true); @@ -542,7 +579,7 @@ var LeafForm = function (containerID) { elsChild.removeAttr('aria-hidden'); elsChild.show(); } else { - clearValues(childFormat, childID); + clearValues(childID); elChildResponse.classList.add('response-hidden'); elsChild.hide(); elsChild.attr('aria-hidden', true); @@ -611,7 +648,7 @@ var LeafForm = function (containerID) { setTimeout(() => { const closestHidden = elChildResponse.closest('.response-hidden'); if (closestHidden !== null) { - clearValues(childFormat, childID); + clearValues(childID); } elChildInput.trigger("change"); diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 9eaf580b2..36357ace0 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -1706,7 +1706,6 @@ function count_required(array $dataTable, array $formNode, bool $parentOrSelfHid count_required($dataTable, $page, false, $requiredTotal, $requiredAnswered); } - error_log("R A: ".$requiredTotal." ".$requiredAnswered); if ($requiredTotal === 0) { $returnValue = 100; } else { From a8f67b9bd3b3a845c827f92c42c6398aea309b57 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Wed, 27 Nov 2024 12:11:03 -0500 Subject: [PATCH 3/6] LEAF 4604 move count_required to own method, update param --- LEAF_Request_Portal/sources/Form.php | 321 +++++++++++++-------------- 1 file changed, 160 insertions(+), 161 deletions(-) diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 36357ace0..172b47ab8 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -1509,6 +1509,163 @@ public function doSubmit(int $recordID): array return $return_value; } + private function count_required(array &$dataTable, array $formNode, bool $parentOrSelfHidden = false, int &$required_total, int &$required_answered) + { + //don't care about any of this if the question is in a hidden state + if($parentOrSelfHidden === false) { + //Check for conditions and if the state is hidden. + $format = trim(strtolower($formNode['format'])); + if(!empty($formNode['conditions']) && $formNode['conditions'] !== 'null') { + $conditions = json_decode(strip_tags($formNode['conditions'])); + $multiChoiceParentFormats = array('multiselect', 'checkboxes'); + $singleChoiceParentFormats = array('radio', 'dropdown', 'number', 'currency'); + $conditionMet = false; + foreach ($conditions as $c) { + if ($c->childFormat === $format && + (strtolower($c->selectedOutcome) === 'hide' || strtolower($c->selectedOutcome) === 'show')) { + + $parentFormat = $c->parentFormat; + $conditionParentValue = preg_split('/\R/', $c->selectedParentValue) ?? []; + $currentParentDataValue = preg_replace('/'/', ''', $dataTable[$c->parentIndID] ?? ''); + if ($parentFormat === 'checkbox') { //single checkbox ifthen is either checked or not checked + $currentParentDataValue = !empty($currentParentDataValue) && $currentParentDataValue !== 'no' ? '1' : '0'; + } + if (in_array($parentFormat, $multiChoiceParentFormats)) { + $currentParentDataValue = @unserialize($currentParentDataValue) === false ? + array($currentParentDataValue) : unserialize($currentParentDataValue); + } else { + $currentParentDataValue = array($currentParentDataValue); + } + + $operator = $c->selectedOp; + switch ($operator) { + case '==': + case '!=': + if (in_array($parentFormat, $multiChoiceParentFormats)) { + //true if the current data value includes any of the condition values + foreach ($currentParentDataValue as $v) { + if (in_array($v, $conditionParentValue)) { + $conditionMet = true; + break; + } + } + } else if (in_array($parentFormat, $singleChoiceParentFormats) && $currentParentDataValue[0] === $conditionParentValue[0]) { + $conditionMet = true; + } + if($operator === "!=") { + $conditionMet = !$conditionMet; + } + break; + case 'gt': + case 'gte': + case 'lt': + case 'lte': + $arrNumVals = array(); + $arrNumComp = array(); + foreach($currentParentDataValue as $v) { + if(is_numeric($v)) { + $arrNumVals[] = (float) $v; + } + } + foreach($conditionParentValue as $cval) { + if(is_numeric($cval)) { + $arrNumComp[] = (float) $cval; + } + } + $useOrEqual = str_contains($operator, 'e'); + $useGreaterThan = str_contains($operator, 'g'); + $lenValues = count(array_values($arrNumVals)); + $lenCompare = count(array_values($arrNumComp)); + if($lenCompare > 0) { + for ($i = 0; $i < $lenValues; $i++) { + $currVal = $arrNumVals[$i]; + if($useGreaterThan === true) { + $conditionMet = $useOrEqual === true ? $currVal >= max($arrNumComp) : $currVal > max($arrNumComp); + } else { + $conditionMet = $useOrEqual === true ? $currVal <= min($arrNumComp) : $currVal < min($arrNumComp); + } + if($conditionMet === true) { + break; + } + } + } + break; + default: + break; + } + } + //if in hidden state, set parenthidden to true and break out of condition checking. + if (($conditionMet === false && strtolower($c->selectedOutcome) === 'show') || + ($conditionMet === true && strtolower($c->selectedOutcome) === 'hide')) { + $parentOrSelfHidden = true; + break; + } + } + } + + //if still not in hidden state and required: increment required total. Check for answer and increment answered total if answered. + if(!$parentOrSelfHidden && (int)$formNode['required'] === 1) { + $required_total += 1; + $answered = false; + $val = $formNode['value']; + if(isset($val) && $val !== '') { + /*value property is already processed based on format and could be string or array. + Convert to array for more consistent comparison and to simplify checkbox(es) and grid formats.*/ + $valType = gettype($val); + + $arrVal = array(); + if($valType === 'array') { + $arrVal = $val; + } elseif ($valType === 'string') { + $val = trim($val); + $arrVal = @unserialize($val) === false ? array($val) : unserialize($val); + } + //these formats can have values despite being unanswered ('no', or serialized data about the entry) + $specialFormat = $format === 'checkbox' || $format === 'checkboxes' || $format === 'grid'; + if ($specialFormat === true) { + if($format === 'grid') { + $hasInput = isset($arrVal['cells']); + $gridContainsEmptyValue = !$hasInput; + if($hasInput) { + $arrRowEntries = $arrVal['cells']; + foreach ($arrRowEntries as $row) { + if (in_array("", $row)) { + $gridContainsEmptyValue = true; + break; + } + } + } + $answered = !$gridContainsEmptyValue; + + } else { + //checkbox(es) - only one needed + foreach($arrVal as $ele) { + if ($ele !== '' && $ele !== 'no') { + $answered = true; + break; + } + } + } + + //any other format, confirm the element is not an empty string + } else { + $answered = $arrVal[0] !== ''; + } + } + + if($answered === true) { + $required_answered += 1; + } + } + } + //progress tree depth. + if(isset($formNode['child'])) { + foreach($formNode['child'] as $child) { + $this->count_required($dataTable, $child, $parentOrSelfHidden, $required_total, $required_answered); + } + } + } + /** * Get the progress percentage (as integer), accounting for conditinally hidden questions * @param int $recordID @@ -1527,12 +1684,11 @@ public function getProgress($recordID) return 100; } } - - $dataSQL = 'SELECT indicatorID, `format`, `data` FROM `data` + $dataSQL = "SELECT `indicatorID`, `format`, `data` FROM `data` LEFT JOIN indicators USING (indicatorID) WHERE recordID=:recordID AND indicators.disabled = 0 - AND `data` != ""'; + AND `data` != ''"; $dataVars = array(':recordID' => (int)$recordID); $resData = $this->db->prepared_query($dataSQL, $dataVars); @@ -1544,166 +1700,9 @@ public function getProgress($recordID) $requiredTotal = 0; $requiredAnswered = 0; - function count_required(array $dataTable, array $formNode, bool $parentOrSelfHidden = false, int &$required_total, int &$required_answered) - { - //don't care about any of this if the question is in a hidden state - if($parentOrSelfHidden === false) { - //Check for conditions and if the state is hidden. - $format = trim(strtolower($formNode['format'])); - if(!empty($formNode['conditions']) && $formNode['conditions'] !== 'null') { - $conditions = json_decode(strip_tags($formNode['conditions'])); - $multiChoiceParentFormats = array('multiselect', 'checkboxes'); - $singleChoiceParentFormats = array('radio', 'dropdown', 'number', 'currency'); - $conditionMet = false; - foreach ($conditions as $c) { - if ($c->childFormat === $format && - (strtolower($c->selectedOutcome) === 'hide' || strtolower($c->selectedOutcome) === 'show')) { - - $parentFormat = $c->parentFormat; - $conditionParentValue = preg_split('/\R/', $c->selectedParentValue) ?? []; - $currentParentDataValue = preg_replace('/'/', ''', $dataTable[$c->parentIndID] ?? ''); - if ($parentFormat === 'checkbox') { //single checkbox ifthen is either checked or not checked - $currentParentDataValue = !empty($currentParentDataValue) && $currentParentDataValue !== 'no' ? '1' : '0'; - } - if (in_array($parentFormat, $multiChoiceParentFormats)) { - $currentParentDataValue = @unserialize($currentParentDataValue) === false ? - array($currentParentDataValue) : unserialize($currentParentDataValue); - } else { - $currentParentDataValue = array($currentParentDataValue); - } - - $operator = $c->selectedOp; - switch ($operator) { - case '==': - case '!=': - if (in_array($parentFormat, $multiChoiceParentFormats)) { - //true if the current data value includes any of the condition values - foreach ($currentParentDataValue as $v) { - if (in_array($v, $conditionParentValue)) { - $conditionMet = true; - break; - } - } - } else if (in_array($parentFormat, $singleChoiceParentFormats) && $currentParentDataValue[0] === $conditionParentValue[0]) { - $conditionMet = true; - } - if($operator === "!=") { - $conditionMet = !$conditionMet; - } - break; - case 'gt': - case 'gte': - case 'lt': - case 'lte': - $arrNumVals = array(); - $arrNumComp = array(); - foreach($currentParentDataValue as $v) { - if(is_numeric($v)) { - $arrNumVals[] = (float) $v; - } - } - foreach($conditionParentValue as $cval) { - if(is_numeric($cval)) { - $arrNumComp[] = (float) $cval; - } - } - $useOrEqual = str_contains($operator, 'e'); - $useGreaterThan = str_contains($operator, 'g'); - $lenValues = count(array_values($arrNumVals)); - $lenCompare = count(array_values($arrNumComp)); - if($lenCompare > 0) { - for ($i = 0; $i < $lenValues; $i++) { - $currVal = $arrNumVals[$i]; - if($useGreaterThan === true) { - $conditionMet = $useOrEqual === true ? $currVal >= max($arrNumComp) : $currVal > max($arrNumComp); - } else { - $conditionMet = $useOrEqual === true ? $currVal <= min($arrNumComp) : $currVal < min($arrNumComp); - } - if($conditionMet === true) { - break; - } - } - } - break; - default: - break; - } - } - //if in hidden state, set parenthidden to true and break out of condition checking. - if (($conditionMet === false && strtolower($c->selectedOutcome) === 'show') || - ($conditionMet === true && strtolower($c->selectedOutcome) === 'hide')) { - $parentOrSelfHidden = true; - break; - } - } - } - - //if still not in hidden state and required: increment required total. Check for answer and increment answered total if answered. - if(!$parentOrSelfHidden && (int)$formNode['required'] === 1) { - $required_total += 1; - $answered = false; - $val = $formNode['value']; - if(isset($val) && $val !== '') { - /*value property is already processed based on format and could be string or array. - Convert to array for more consistent comparison and to simplify checkbox(es) and grid formats.*/ - $valType = gettype($val); - - $arrVal = array(); - if($valType === 'array') { - $arrVal = $val; - } elseif ($valType === 'string') { - $val = trim($val); - $arrVal = @unserialize($val) === false ? array($val) : unserialize($val); - } - //these formats can have values despite being unanswered ('no', or serialized data about the entry) - $specialFormat = $format === 'checkbox' || $format === 'checkboxes' || $format === 'grid'; - if ($specialFormat === true) { - if($format === 'grid') { - $hasInput = isset($arrVal['cells']); - $gridContainsEmptyValue = !$hasInput; - if($hasInput) { - $arrRowEntries = $arrVal['cells']; - foreach ($arrRowEntries as $row) { - if (in_array("", $row)) { - $gridContainsEmptyValue = true; - break; - } - } - } - $answered = !$gridContainsEmptyValue; - - } else { - //checkbox(es) - only one needed - foreach($arrVal as $ele) { - if ($ele !== '' && $ele !== 'no') { - $answered = true; - break; - } - } - } - - //any other format, confirm the element is not an empty string - } else { - $answered = $arrVal[0] !== ''; - } - } - - if($answered === true) { - $required_answered += 1; - } - } - } - //progress tree depth. - if(isset($formNode['child'])) { - foreach($formNode['child'] as $child) { - count_required($dataTable, $child, $parentOrSelfHidden, $required_total, $required_answered); - } - } - } - $fullForm = $this->getFullForm($recordID); foreach($fullForm as $page) { - count_required($dataTable, $page, false, $requiredTotal, $requiredAnswered); + $this->count_required($dataTable, $page, false, $requiredTotal, $requiredAnswered); } if ($requiredTotal === 0) { From e20c490e976661460acbd2e040e2ffda1ab36540 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Tue, 3 Dec 2024 07:29:37 -0500 Subject: [PATCH 4/6] LEAF 4604 rename some variables, add total tracking --- LEAF-Automated-Tests | 2 +- LEAF_Request_Portal/sources/Form.php | 79 +++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/LEAF-Automated-Tests b/LEAF-Automated-Tests index 8d87883ef..a7f6435a6 160000 --- a/LEAF-Automated-Tests +++ b/LEAF-Automated-Tests @@ -1 +1 @@ -Subproject commit 8d87883efbef6bf4e028868c2db41ec42c2d0a35 +Subproject commit a7f6435a65ae82b646ebfd70cd3ea92015b96a38 diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 172b47ab8..6d413b10c 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -1509,8 +1509,29 @@ public function doSubmit(int $recordID): array return $return_value; } - private function count_required(array &$dataTable, array $formNode, bool $parentOrSelfHidden = false, int &$required_total, int &$required_answered) + /** + * Keep track of number of required questions, number visible, and form branch visibility state + * + * @param array dataTable (ref) lookup table of form data. Used to check if question is answered and for conditional logic assessment + * @param array formNode form question with potential substructure + * @param bool parentOrSelfHidden is question and children in hidden state + * @param int required_visible (ref) number of required questions that are visible + * @param int required_answered (ref) number of required questions that are answered + * @param int required_total (ref) total number of required questions found during checking + * @param int res_max_required total number of required questions on the form + */ + private function count_required( + array &$dataTable, + array $formNode, + bool $parentOrSelfHidden = false, + int &$required_visible, + int &$required_answered, + int &$required_total, + int $res_max_required) { + if((int)$formNode['required'] === 1) { + $required_total += 1; //keep track to skip calls once all required questions are found. + } //don't care about any of this if the question is in a hidden state if($parentOrSelfHidden === false) { //Check for conditions and if the state is hidden. @@ -1601,11 +1622,12 @@ private function count_required(array &$dataTable, array $formNode, bool $parent break; } } + unset($conditions); } - //if still not in hidden state and required: increment required total. Check for answer and increment answered total if answered. + //if not in hidden state and required: increment required total. Check for answer and increment answered total if answered. if(!$parentOrSelfHidden && (int)$formNode['required'] === 1) { - $required_total += 1; + $required_visible += 1; $answered = false; $val = $formNode['value']; if(isset($val) && $val !== '') { @@ -1658,10 +1680,20 @@ private function count_required(array &$dataTable, array $formNode, bool $parent } } } - //progress tree depth. + //progress tree depth if required total is not at max. if(isset($formNode['child'])) { foreach($formNode['child'] as $child) { - $this->count_required($dataTable, $child, $parentOrSelfHidden, $required_total, $required_answered); + if($required_total < $res_max_required) { + $this->count_required( + $dataTable, + $child, + $parentOrSelfHidden, + $required_visible, + $required_answered, + $required_total, + $res_max_required + ); + } } } } @@ -1673,22 +1705,39 @@ private function count_required(array &$dataTable, array $formNode, bool $parent */ public function getProgress($recordID) { - $subSQL = 'SELECT submitted FROM records + $subSQL = 'SELECT submitted, categoryID FROM records LEFT JOIN category_count USING (recordID) WHERE recordID=:recordID'; $subVars = array(':recordID' => (int)$recordID); - //First check if submitted and just return 100 if it is. + $resRecordInfoEachForm = $this->db->prepared_query($subSQL, $subVars); + + //Check if submitted, return 100 if it is. Get catIDs for otherwise. + $categoryIDs = array(); foreach ($resRecordInfoEachForm as $request) { + $categoryIDs[] = $request['categoryID']; if ($request['submitted'] > 0) { return 100; } } + + $maxRequiredSQL = "SELECT `indicatorID` FROM `indicators` + WHERE `required`=1 AND `disabled`=0 AND FIND_IN_SET(categoryID, :categoryIDs)"; + $maxRequiredVars = array( + ":categoryIDs" => implode(",", $categoryIDs) + ); + $resMaxRequired = count($this->db->prepared_query($maxRequiredSQL, $maxRequiredVars)); + + //Check max count. Return 100 if there are none. Otherwise use this count for total checking. + if ($resMaxRequired === 0) { + return 100; + } + $dataSQL = "SELECT `indicatorID`, `format`, `data` FROM `data` LEFT JOIN indicators USING (indicatorID) WHERE recordID=:recordID AND indicators.disabled = 0 - AND `data` != ''"; + AND TRIM(`data`) != ''"; $dataVars = array(':recordID' => (int)$recordID); $resData = $this->db->prepared_query($dataSQL, $dataVars); @@ -1696,19 +1745,23 @@ public function getProgress($recordID) foreach($resData as $d) { $dataTable[$d['indicatorID']] = $d['data']; } - $returnValue = 0; - $requiredTotal = 0; + + $requiredVisible = 0; $requiredAnswered = 0; + $requiredTotal = 0; $fullForm = $this->getFullForm($recordID); foreach($fullForm as $page) { - $this->count_required($dataTable, $page, false, $requiredTotal, $requiredAnswered); + if($requiredTotal < $resMaxRequired) { + $this->count_required($dataTable, $page, false, $requiredVisible, $requiredAnswered, $requiredTotal, $resMaxRequired); + } } - if ($requiredTotal === 0) { + $returnValue = 0; + if ($requiredVisible === 0) { $returnValue = 100; } else { - $returnValue = round(100 * ($requiredAnswered / $requiredTotal)); + $returnValue = round(100 * ($requiredAnswered / $requiredVisible)); } return $returnValue; } From aeec897b53fd56f06dbfa137d555ff08d4cb5c87 Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Tue, 3 Dec 2024 07:53:21 -0500 Subject: [PATCH 5/6] LEAF 4604 comment unused / pending checkbox logic, add trim to data value --- LEAF_Request_Portal/sources/Form.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index 6d413b10c..d0061f888 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -1548,9 +1548,9 @@ private function count_required( $parentFormat = $c->parentFormat; $conditionParentValue = preg_split('/\R/', $c->selectedParentValue) ?? []; $currentParentDataValue = preg_replace('/'/', ''', $dataTable[$c->parentIndID] ?? ''); - if ($parentFormat === 'checkbox') { //single checkbox ifthen is either checked or not checked + /* if ($parentFormat === 'checkbox') { //single checkbox ifthen is either checked or not checked (pending parent checkbox) $currentParentDataValue = !empty($currentParentDataValue) && $currentParentDataValue !== 'no' ? '1' : '0'; - } + } */ if (in_array($parentFormat, $multiChoiceParentFormats)) { $currentParentDataValue = @unserialize($currentParentDataValue) === false ? array($currentParentDataValue) : unserialize($currentParentDataValue); @@ -1733,7 +1733,7 @@ public function getProgress($recordID) return 100; } - $dataSQL = "SELECT `indicatorID`, `format`, `data` FROM `data` + $dataSQL = "SELECT `indicatorID`, `format`, TRIM(`data`) as `data` FROM `data` LEFT JOIN indicators USING (indicatorID) WHERE recordID=:recordID AND indicators.disabled = 0 From fdd9aa07df99ae7f5226243e8422ba061f3dc92d Mon Sep 17 00:00:00 2001 From: Carrie Hanscom Date: Wed, 4 Dec 2024 14:49:31 -0500 Subject: [PATCH 6/6] LEAF 4604 method signature per feedback --- LEAF_Request_Portal/ajaxJSON.php | 2 +- LEAF_Request_Portal/api/controllers/FormController.php | 2 +- LEAF_Request_Portal/sources/Form.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LEAF_Request_Portal/ajaxJSON.php b/LEAF_Request_Portal/ajaxJSON.php index 00ded8753..16454291a 100644 --- a/LEAF_Request_Portal/ajaxJSON.php +++ b/LEAF_Request_Portal/ajaxJSON.php @@ -32,7 +32,7 @@ // this method does not exist in Form class // echo $form->getProgressJSON($_GET['recordID']); // but this one does - echo $form->getProgress($_GET['recordID']); + echo $form->getProgress((int)$_GET['recordID']); break; case 'getrecentactions': diff --git a/LEAF_Request_Portal/api/controllers/FormController.php b/LEAF_Request_Portal/api/controllers/FormController.php index 36c35ef50..4c862b198 100644 --- a/LEAF_Request_Portal/api/controllers/FormController.php +++ b/LEAF_Request_Portal/api/controllers/FormController.php @@ -121,7 +121,7 @@ public function get($act) }); $this->index['GET']->register('form/[digit]/progress', function ($args) use ($form) { - $return = $form->getProgress($args[0]); + $return = $form->getProgress((int)$args[0]); return $return; }); diff --git a/LEAF_Request_Portal/sources/Form.php b/LEAF_Request_Portal/sources/Form.php index d0061f888..d92116efd 100644 --- a/LEAF_Request_Portal/sources/Form.php +++ b/LEAF_Request_Portal/sources/Form.php @@ -1527,7 +1527,7 @@ private function count_required( int &$required_visible, int &$required_answered, int &$required_total, - int $res_max_required) + int $res_max_required): void { if((int)$formNode['required'] === 1) { $required_total += 1; //keep track to skip calls once all required questions are found. @@ -1703,7 +1703,7 @@ private function count_required( * @param int $recordID * @return int Percent completed */ - public function getProgress($recordID) + public function getProgress(int $recordID): int { $subSQL = 'SELECT submitted, categoryID FROM records LEFT JOIN category_count USING (recordID)