diff --git a/admin/database.php b/admin/database.php index f9640c21510..672c4e237ef 100644 --- a/admin/database.php +++ b/admin/database.php @@ -1057,7 +1057,7 @@ function get_max_question_order($gid) db_switchIDInsert('questions',true); $query='INSERT into '.db_table_name('questions').' (qid, sid, gid, question_order, title, question, parent_qid, language, scale_id) values ('.$insertqid[$scale_id][$position].','.$surveyid.','.$gid.','.($position+1).','.db_quoteall($codes[$scale_id][$position]).','.db_quoteall($subquestionvalue).','.$qid.','.db_quoteall($language).','.$scale_id.')'; $connect->execute($query); - db_switchIDInsert('questions',true); + db_switchIDInsert('questions',false); } } $position++; diff --git a/admin/scripts/updateset.js b/admin/scripts/updateset.js index 257efcd8ab4..64ab5de05ac 100644 --- a/admin/scripts/updateset.js +++ b/admin/scripts/updateset.js @@ -1,5 +1,3 @@ -// $Id$ - $(document).ready(function(){ if ($(".answertable tbody").children().length == 0) @@ -160,8 +158,14 @@ function sort_complete(event, ui){ function add_label(event) { - - next_code=getNextCode($(this).parent().parent().find('.codeval').val()); + if ($(this).parent().parent().find('.codeval').size()>0) + { + next_code=getNextCode($(this).parent().parent().find('.codeval').val()); + } + else + { + next_code='L001'; + } while ($(this).parent().parent().parent().find('input[value="'+next_code+'"]').length>0) { next_code=getNextCode(next_code); diff --git a/classes/expressions/LimeExpressionManager.php b/classes/expressions/LimeExpressionManager.php index be96a4dde97..df6efb4fb7b 100644 --- a/classes/expressions/LimeExpressionManager.php +++ b/classes/expressions/LimeExpressionManager.php @@ -461,6 +461,7 @@ class LimeExpressionManager { * 'varName' => 'afSrcFilter_sq1' // the full qcode variable name - note, if there are sub-questions, don't use this one. * 'type' => 'M' // the one-letter question type * 'fieldname' => '26626X34X702sq1' // the fieldname (used as JavaScript variable name, and also as database column name + * 'rootVarName' => 'afDS' // the root variable name * 'subqs' => array() of sub-questions, where each contains: * 'rowdivid' => '26626X34X702sq1' // the javascript id identifying the question row (so array_filter can hide rows) * 'varName' => 'afSrcFilter_sq1' // the full variable name for the sub-question @@ -602,6 +603,11 @@ class LimeExpressionManager { * @var integer */ private $numQuestions=0; + /** + * Linked list of array filters + * @var array + */ + private $qrootVarName2arrayFilter = array(); /** * A private constructor; prevents direct creation of object @@ -959,67 +965,39 @@ public function _CreateSubQLevelRelevanceAndValidationEqns($onlyThisQseq=NULL) // array_filter // If want to filter question Q2 on Q1, where each have subquestions SQ1-SQ3, this is equivalent to relevance equations of: // relevance for Q2_SQ1 is Q1_SQ1!='' + $array_filter = NULL; if (isset($qattr['array_filter']) && trim($qattr['array_filter']) != '') { $array_filter = $qattr['array_filter']; - $sgq = ((isset($this->qcode2sgq[$array_filter])) ? $this->qcode2sgq[$array_filter] : $array_filter); - if ($hasSubqs) { - $subqs = $qinfo['subqs']; - foreach ($subqs as $sq) { - $sq_name = NULL; - switch ($type) - { - case '1': //Array (Flexible Labels) dual scale - case ':': //ARRAY (Multi Flexi) 1 to 10 - case ';': //ARRAY (Multi Flexi) Text - case 'A': //ARRAY (5 POINT CHOICE) radio-buttons - case 'B': //ARRAY (10 POINT CHOICE) radio-buttons - case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons - case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons - case 'F': //ARRAY (Flexible) - Row Format - case 'L': //LIST drop-down/radio-button list - case 'M': //Multiple choice checkbox - case 'P': //Multiple choice with comments checkbox + text - case 'K': //MULTIPLE NUMERICAL QUESTION - case 'Q': //MULTIPLE SHORT TEXT - if ($this->sgqaNaming) - { - $sq_name = $sgq . substr($sq['sqsuffix'],1); - } - else - { - $sq_name = $array_filter . $sq['sqsuffix']; - } - break; - default: - break; - } - if (!is_null($sq_name)) { - $subQrels[] = array( - 'qtype' => $type, - 'type' => 'array_filter', - 'rowdivid' => $sq['rowdivid'], - 'eqn' => '(' . $sq_name . ' != "")', - 'qid' => $questionNum, - 'sgqa' => $qinfo['sgqa'], - ); - } - } - } + $this->qrootVarName2arrayFilter[$qinfo['rootVarName']]['array_filter'] = $array_filter; } // array_filter_exclude // If want to filter question Q2 on Q1, where each have subquestions SQ1-SQ3, this is equivalent to relevance equations of: // relevance for Q2_SQ1 is Q1_SQ1=='' + $array_filter_exclude = NULL; if (isset($qattr['array_filter_exclude']) && trim($qattr['array_filter_exclude']) != '') { $array_filter_exclude = $qattr['array_filter_exclude']; - $sgq = ((isset($this->qcode2sgq[$array_filter_exclude])) ? $this->qcode2sgq[$array_filter_exclude] : $array_filter_exclude); + $this->qrootVarName2arrayFilter[$qinfo['rootVarName']]['array_filter_exclude'] = $array_filter_exclude; + } + + // array_filter and array_filter_exclude get processed together + if (!is_null($array_filter) || !is_null($array_filter_exclude)) + { if ($hasSubqs) { + $cascadedAF = array(); + $cascadedAFE = array(); + + list($cascadedAF, $cascadedAFE) = $this->_recursivelyFindAntecdentArrayFilters($qinfo['rootVarName'],array(),array()); + + $cascadedAF = array_reverse($cascadedAF); + $cascadedAFE = array_reverse($cascadedAFE); + $subqs = $qinfo['subqs']; - $sq_names = array(); foreach ($subqs as $sq) { - $sq_name = NULL; + $af_names = array(); + $afe_names = array(); switch ($type) { case '1': //Array (Flexible Labels) dual scale @@ -1037,22 +1015,55 @@ public function _CreateSubQLevelRelevanceAndValidationEqns($onlyThisQseq=NULL) case 'Q': //MULTIPLE SHORT TEXT if ($this->sgqaNaming) { - $sq_name = $sgq . substr($sq['sqsuffix'],1); + foreach ($cascadedAF as $_caf) + { + $sgq = ((isset($this->qcode2sgq[$_caf])) ? $this->qcode2sgq[$_caf] : $_caf); + $af_names[] = $sgq . substr($sq['sqsuffix'],1);; + } + foreach ($cascadedAFE as $_cafe) + { + $sgq = ((isset($this->qcode2sgq[$_cafe])) ? $this->qcode2sgq[$_cafe] : $_cafe); + $afe_names[] = $sgq . substr($sq['sqsuffix'],1);; + } } else { - $sq_name = $array_filter_exclude . $sq['sqsuffix']; + foreach ($cascadedAF as $_caf) + { + $af_names[] = $_caf . $sq['sqsuffix']; + } + foreach ($cascadedAFE as $_cafe) + { + $afe_names[] = $_cafe . $sq['sqsuffix']; + } } break; default: break; } - if (!is_null($sq_name)) { + $af_names = array_unique($af_names); + $afe_names= array_unique($afe_names); + + if (count($af_names) > 0 || count($afe_names) > 0) { + $afs_eqn = ''; + if (count($af_names) > 0) + { + $afs_eqn .= implode(' != "" && ', $af_names) . ' != ""'; + } + if (count($afe_names) > 0) + { + if ($afs_eqn != '') + { + $afs_eqn .= ' && '; + } + $afs_eqn .= implode(' == "" && ', array_unique($afe_names)) . ' == ""'; + } + $subQrels[] = array( 'qtype' => $type, - 'type' => 'array_filter_exclude', + 'type' => 'array_filter', 'rowdivid' => $sq['rowdivid'], - 'eqn' => '(' . $sq_name . ' == "")', + 'eqn' => '(' . $afs_eqn . ')', 'qid' => $questionNum, 'sgqa' => $qinfo['sgqa'], ); @@ -2319,6 +2330,50 @@ public function _CreateSubQLevelRelevanceAndValidationEqns($onlyThisQseq=NULL) // $this->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now)); } + /** + * Recursively find all questions that logically preceded the current array_filter or array_filter_exclude request + * Note, must support: + * (a) semicolon-separated list of $qroot codes for either array_filter or array_filter_exclude + * (b) mixed history of array_filter and array_filter_exclude values + * @param type $qroot - the question root variable name + * @param type $aflist - the list of array_filter $qroot codes + * @param type $afelist - the list of array_filter_exclude $qroot codes + * @return type + */ + private function _recursivelyFindAntecdentArrayFilters($qroot, $aflist, $afelist) + { + if (isset($this->qrootVarName2arrayFilter[$qroot])) + { + if (isset($this->qrootVarName2arrayFilter[$qroot]['array_filter'])) + { + $_afs = explode(';',$this->qrootVarName2arrayFilter[$qroot]['array_filter']); + foreach ($_afs as $_af) + { + if (in_array($_af,$aflist)) + { + continue; + } + $aflist[] = $_af; + list($aflist, $afelist) = $this->_recursivelyFindAntecdentArrayFilters($_af, $aflist, $afelist); + } + } + if (isset($this->qrootVarName2arrayFilter[$qroot]['array_filter_exclude'])) + { + $_afes = explode(';',$this->qrootVarName2arrayFilter[$qroot]['array_filter_exclude']); + foreach ($_afes as $_afe) + { + if (in_array($_afe,$afelist)) + { + continue; + } + $afelist[] = $_afe; + list($aflist, $afelist) = $this->_recursivelyFindAntecdentArrayFilters($_afe, $aflist, $afelist); + } + } + } + return array($aflist, $afelist); + } + /** * Create the arrays needed by ExpressionManager to process LimeSurvey strings. * The long part of this function should only be called once per page display (e.g. only if $fieldMap changes) @@ -2770,6 +2825,7 @@ private function setVariableAndTokenMappingsForExpressionManager($surveyid,$forc 'type' => $type, 'fieldname' => $sgqa, 'preg' => $preg, + 'rootVarName' => $fielddata['title'], ); } if (!isset($q2subqInfo[$questionNum]['subqs'])) { @@ -3433,6 +3489,7 @@ static function StartSurvey($surveyid,$surveyMode='group',$options=NULL,$forceRe $LEM->currentQuestionSeq=-1; // for question-by-question mode $LEM->indexGseq=array(); $LEM->indexQseq=array(); + $LEM->qrootVarName2arrayFilter=array(); if (isset($_SESSION['startingValues']) && is_array($_SESSION['startingValues']) && count($_SESSION['startingValues']) > 0) { @@ -5554,40 +5611,32 @@ static function GetRelevanceAndTailoringJavaScript() foreach ($subqParts as $sq) { $rowdividList[$sq['rowdivid']] = $sq['result']; - if ($sq['result'] == false && !($LEM->surveyMode == 'survey')) - { - // then cascading array filter and can never be active - $relParts[] = " $('#relevance" . $sq['rowdivid'] . "').val('');\n"; - } - else + + $relParts[] = " if ( " . $sq['relevancejs'] . " ) {\n"; + $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "').show();\n"; + $relParts[] = " if ($('#relevance" . $sq['rowdivid'] . "').val()!='1') { relChange" . $arg['qid'] . "=true; }\n"; + $relParts[] = " $('#relevance" . $sq['rowdivid'] . "').val('1');\n"; + $relParts[] = " }\n else {\n"; + $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "').hide();\n"; + $relParts[] = " if ($('#relevance" . $sq['rowdivid'] . "').val()=='1') { relChange" . $arg['qid'] . "=true; }\n"; + $relParts[] = " $('#relevance" . $sq['rowdivid'] . "').val('');\n"; + switch ($sq['qtype']) { - // $relParts[] = " // Apply " . $sq['type'] . ": " . $sq['eqn'] ."\n"; - $relParts[] = " if ( " . $sq['relevancejs'] . " ) {\n"; - $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "').show();\n"; - $relParts[] = " if ($('#relevance" . $sq['rowdivid'] . "').val()!='1') { relChange" . $arg['qid'] . "=true; }\n"; - $relParts[] = " $('#relevance" . $sq['rowdivid'] . "').val('1');\n"; - $relParts[] = " }\n else {\n"; - $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "').hide();\n"; - $relParts[] = " if ($('#relevance" . $sq['rowdivid'] . "').val()=='1') { relChange" . $arg['qid'] . "=true; }\n"; - $relParts[] = " $('#relevance" . $sq['rowdivid'] . "').val('');\n"; - switch ($sq['qtype']) - { - case 'L': //LIST drop-down/radio-button list - $listItem = substr($sq['rowdivid'],strlen($sq['sgqa'])); // gets the part of the rowdiv id past the end of the sgqa code. - $relParts[] = " if (($('#java" . $sq['sgqa'] ."').val() == '" . $listItem . "')"; - if ($listItem == 'other') { - $relParts[] = " || ($('#java" . $sq['sgqa'] ."').val() == '-oth-')"; - } - $relParts[] = "){\n"; - $relParts[] = " $('#java" . $sq['sgqa'] . "').val('');\n"; - $relParts[] = " $('#answer" . $sq['sgqa'] . "NANS').attr('checked',true);\n"; - $relParts[] = " }\n"; - break; - default: - break; - } - $relParts[] = " }\n"; + case 'L': //LIST drop-down/radio-button list + $listItem = substr($sq['rowdivid'],strlen($sq['sgqa'])); // gets the part of the rowdiv id past the end of the sgqa code. + $relParts[] = " if (($('#java" . $sq['sgqa'] ."').val() == '" . $listItem . "')"; + if ($listItem == 'other') { + $relParts[] = " || ($('#java" . $sq['sgqa'] ."').val() == '-oth-')"; + } + $relParts[] = "){\n"; + $relParts[] = " $('#java" . $sq['sgqa'] . "').val('');\n"; + $relParts[] = " $('#answer" . $sq['sgqa'] . "NANS').attr('checked',true);\n"; + $relParts[] = " }\n"; + break; + default: + break; } + $relParts[] = " }\n"; $sqvars = explode('|',$sq['relevanceVars']); if (is_array($sqvars)) diff --git a/common_functions.php b/common_functions.php index 611cf13fc4a..497545029ce 100644 --- a/common_functions.php +++ b/common_functions.php @@ -3222,7 +3222,7 @@ function questionAttributes($returnByName=false) 'category'=>$clang->gT('Logic'), 'sortorder'=>100, 'inputtype'=>'text', - "help"=>$clang->gT("Enter the code of a Multiple choice question to only show the matching answer options in this question."), + "help"=>$clang->gT("Enter the code(s) of a Multiple choice question(s) (separated by semicolons) to only show the matching answer options in this question."), "caption"=>$clang->gT('Array filter')); $qattributes["array_filter_exclude"]=array( @@ -3230,7 +3230,7 @@ function questionAttributes($returnByName=false) 'category'=>$clang->gT('Logic'), 'sortorder'=>100, 'inputtype'=>'text', - "help"=>$clang->gT("Enter the code of a Multiple choice question to exclude the matching answer options in this question."), + "help"=>$clang->gT("Enter the code(s) of a Multiple choice question(s) (separated by semicolons) to exclude the matching answer options in this question."), "caption"=>$clang->gT('Array filter exclusion')); $qattributes["assessment_value"]=array(