Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
10431 lines (9916 sloc) 529 KB
<?php
/**
* LimeSurvey
* Copyright (C) 2007-2015 The LimeSurvey Project Team / Carsten Schmitz
* All rights reserved.
* License: GNU/GPL License v2 or later, see LICENSE.php
* LimeSurvey is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
* See COPYRIGHT.php for copyright notices and details.
*
*/
/**
* LimeExpressionManager
* This is a wrapper class around ExpressionManager that implements a Singleton and eases
* passing of LimeSurvey variable values into ExpressionManager
*
* @author LimeSurvey Team (limesurvey.org)
* @author Thomas M. White (TMSWhite)
* @author Denis Chenu <http://sondages.pro>
*/
Yii::import('application.helpers.expressions.em_core_helper', true);
Yii::app()->loadHelper('database');
Yii::app()->loadHelper('frontend');
Yii::app()->loadHelper('surveytranslator');
Yii::import("application.libraries.Date_Time_Converter");
define('LEM_DEBUG_TIMING',1);
define('LEM_DEBUG_VALIDATION_SUMMARY',2); // also includes SQL error messages
define('LEM_DEBUG_VALIDATION_DETAIL',4);
define('LEM_PRETTY_PRINT_ALL_SYNTAX',32);
define('LEM_DEFAULT_PRECISION',12);
class LimeExpressionManager {
/**
* LimeExpressionManager is a singleton. $instance is its storage location.
* @var LimeExpressionManager
*/
private static $instance;
/**
* Implements the recursive descent parser that processes expressions
* @var ExpressionManager
*/
private $em;
/**
*
* @var array
*/
private $groupRelevanceInfo;
/**
* The survey ID
* @var integer
*/
private $sid;
/**
* sum of LEM_DEBUG constants - use bitwise AND comparisons to identify which parts to use
* @var int
*/
private $debugLevel=0;
/**
* sPreviewMode used for relevance equation and to disable save value in DB
* 'question' or 'group' string force relevance to 1 if needed
* @var string|false
*/
private $sPreviewMode=false;
/**
* Collection of variable attributes, indexed by SGQA code
*
* Actual variables are stored in this structure:
* $knownVars[$sgqa] = array(
* 'jsName_on' => // the name of the javascript variable if it is defined on the current page - often 'answerSGQA'
* 'jsName' => // the name of the javascript variable when referenced on different pages - usually 'javaSGQA'
* 'readWrite' => // 'Y' for yes, 'N' for no - currently not used
* 'hidden' => // 1 if the question attribute 'hidden' is true, otherwise 0
* 'question' => // the text of the question (or subquestion)
* 'qid' => // the numeric question id - e.g. the Q part of the SGQA name
* 'gid' => // the numeric group id - e.g. the G part of the SGQA name
* 'grelevance' => // the group level relevance string
* 'relevance' => // the question level relevance string
* 'qcode' => // the qcode-style variable name for this question (or subquestion)
* 'qseq' => // the 0-based index of the question within the survey
* 'gseq' => // the 0-based index of the group within the survey
* 'type' => // the single character type code for the question
* 'sgqa' => // the SGQA name for the variable
* 'ansList' => // ansArray converted to a JavaScript fragment - e.g. ",'answers':{ 'M':'Male','F':'Female'}"
* 'ansArray' => // PHP array of answer strings, keyed on the answer code = e.g. array['M']='Male';
* 'scale_id' => // '0' for most answers. '1' for second scale within dual-scale questions
* 'rootVarName' => // the root code / name / title for the question, without any subquestion or answer-level suffix. This is from the title column in the questions table
* 'subqtext' => // the subquestion text
* 'rowdivid' => // the JavaScript ID of the row identifier for a question. This is used to show/hide entire question rows
* 'onlynum' => // 1 if only numbers are allowed for this variable. If so, then extra processing is needed to ensure that can use comma as a decimal separator
* );
*
* Reserved variables (e.g. TOKEN:xxxx) are stored with this structure:
* $knownVars[$token] = array(
* 'code' => // the static value for the variable
* 'type' => // ''
* 'jsName_on' => // ''
* 'jsName' => // ''
* 'readWrite' => // 'N' - since these are always read-only variables
* );
*
* @var array
*/
private $knownVars;
/**
* maps qcode varname to SGQA code
*
* @example ['gender'] = '38612X10X145'
* @var array
*/
private $qcode2sgqa;
/**
* variables temporarily set for substitution purposes
*
* These are typically the LimeReplacement Fields passed in via templatereplace()
* Each has the following structure: array(
* 'code' => // the static value of the variable
* 'jsName_on' => // ''
* 'jsName' => // ''
* 'readWrite' => // 'N'
* );
*
* @var array
*/
private $tempVars;
/**
* Array of relevance information for each page (gseq), indexed by gseq.
* Within a page, it contains a sequential list of the results of each relevance equation processed
* array(
* 'qid' => // question id -- e.g. 154
* 'gseq' => // 0-based group sequence -- e.g. 2
* 'eqn' => // the raw relevance equation parsed -- e.g. "!is_empty(p2_sex)"
* 'result' => // the Boolean result of parsing that equation in the current context -- e.g. 0
* 'numJsVars' => // the number of dynamic JavaScript variables used in that equation -- e.g. 1
* 'relevancejs' => // the actual JavaScript to insert for that relevance equation -- e.g. "LEMif(LEManyNA('p2_sex'),'',( ! LEMempty(LEMval('p2_sex') )))"
* 'relevanceVars' => // a pipe-delimited list of JavaScript variables upon which that equation depends -- e.g. "java38612X12X153"
* 'jsResultVar' => // the JavaScript variable in which that result will be stored -- e.g. "java38612X12X154"
* 'type' => // the single character type of the question -- e.g. 'S'
* 'hidden' => // 1 if the question should always be hidden
* 'hasErrors' => // 1 if there were parsing errors processing that relevance equation
* @var array
*/
private $pageRelevanceInfo;
/**
*
* @var array
*/
private $pageTailorInfo;
/**
* internally set to true (1) for survey.php so get group-specific logging but keep javascript variable namings consistent on the page.
* @var boolean
*/
private $allOnOnePage=false;
/**
* survey mode. One of 'survey', 'group', or 'question'
* @var string
*/
private $surveyMode='group';
/**
* a set of global survey options passed from LimeSurvey
*
* For example, array(
* 'rooturl' => // URL prefix needed to be able to click on a syntax-highlighted variable name and have it open the needed editting window
* 'hyperlinkSyntaxHighlighting' => // true if should be able to click on variables to edit them
* 'active' => // 0 for inactive, 1 for active survey
* 'allowsave' => // 0 for do not allow save; 1 for allow save
* 'anonymized' => // 1 for anonymous
* 'assessments' => // 1 for use assessments
* 'datestamp' => // 1 for use date stamps
* 'ipaddr' => // 1 for capture IP address
* 'radix' => // '.' for use period as decimal separator; ',' for use comma as decimal separator
* 'savetimings' => // "Y" if should save survey timings
* 'startlanguage' => // the starting language -- e.g. 'en'
* 'surveyls_dateformat' => // the index of the language specific date format -- e.g. 1
* 'tablename' => // the name of the table storing the survey data, if active -- e.g. lime_survey_38612
* 'target' => // the path for uploading files -- e.g. '/temp/files/'
* 'timeadjust' => // the time offset -- e.g. 0
* 'tempdir' => // the temporary directory for uploading files -- e.g. '/temp/'
* );
*
* @var array
*/
private $surveyOptions=array();
/**
* array of mappings of Question # (qid) to pipe-delimited list of SGQA codes used within it
*
* @example [150] = "38612X11X150|38612X11X150other"
* @var array
*/
private $qid2code;
/**
* array of mappings of JavaScript Variable names to Question number (qid)
*
* @example ['java38612X13X161other'] = '161'
* @var array
*/
private $jsVar2qid;
/**
* maps name of the variable to the SGQ name (without the A suffix)
*
* @example ['p1_sex'] = "38612X10X147"
* @example ['afDS_sq1_1'] = "26626X37X705sq1#1"
* @var array
*/
private $qcode2sgq;
/**
* array of mappings of knownVar aliases to the JavaScript variable names.
* This maps both the SGQA and qcode alias names to the same 2 dimensional array
*
* @example ['p1_sex'] = array(
* 'jsName' => // the JavaScript variable name used by EM -- e.g. "java38612X11X147"
* 'jsPart' => // the JavaScript fragment used in EM's ____ array -- e.g. "'p1_sex':'java38612X11X147'"
* );
* @example ['afDS_sq1_1] = array(
* 'jsName' => "java26626X37X705sq1#1"
* 'jsPart' => "'afDS_sq1_1':'java26626X37X705sq1#1'"
* );
* @var array
*/
private $alias2varName;
/**
* JavaScript array of mappings of canonical JavaScript variable name to key attributes.
* These fragments are used to create the JavaScript varNameAttr array.
*
* @example ['java38612X11X147'] = "'java38612X11X147':{ 'jsName':'java38612X11X147','jsName_on':'java38612X11X147','sgqa':'38612X11X147','qid':147,'gid':11,'type':'G','default':'','rowdivid':'','onlynum':'','gseq':1,'answers':{ 'M':'Male','F':'Female'}}"
* @example ['java26626X37X705sq1#1'] = "'java26626X37X705sq1#1':{ 'jsName':'java26626X37X705sq1#1','jsName_on':'java26626X37X705sq1#1','sgqa':'26626X37X705sq1#1','qid':705,'gid':37,'type':'1','default':'','rowdivid':'26626X37X705sq1','onlynum':'','gseq':1,'answers':{ '0~1':'1|Low','0~2':'2|Medium','0~3':'3|High','1~1':'1|Never','1~2':'2|Sometimes','1~3':'3|Always'}}"
*
* @var array
*/
private $varNameAttr;
/**
* array of enumerated answer lists indexed by qid
* These use a tilde syntax to indicate which scale the answer is part of.
*
* @example ['0~4'] = "4|Child" // this means that code 4 in scale 0 has a coded value of 4 and a display value of 'Child'
* @example (for [705]): ['1~2'] = '2|Sometimes' // this means that the second scale for this question uses the coded value of 2 to represent 'Sometimes'
* @example // TODO - add example from survey using assessments
*
* @var array
*/
private $qans;
/**
* map of gid to 0-based sequence number of groups
*
* @example [10] = 0 // means that the first group (gseq=0) has gid=10
*
* @var array
*/
private $groupId2groupSeq;
/**
* map question # to an incremental count of question order across the whole survey
*
* @example [157] = 13 // means that that 14th question in the survey has qid=157
*
* @var array
*/
private $questionId2questionSeq;
/**
* map question # to the group it is within, using an incremental count of group order
*
* @example [157] = 2 // means that qid 157 is in the 3rd page of questions (gseq = 2)
*
* @var array
*/
private $questionId2groupSeq;
/**
* array of info about each Group, indexed by GroupSeq
*
* @example [2] = array(
* 'qstart' => 9 // the first qseq within that group
* 'qend' => 13 //the last qseq within that group
* );
*
* @var array
*/
private $groupSeqInfo;
/**
* tracks which groups have at least one relevant, non-hidden question
*
* @example [2] = 0 // means that the third group (gseq==2) is currently irrelevant
*
* @var array
*/
private $gseq2relevanceStatus;
/**
* maps question # to the validation equation(s) for that question.
* These are grouped by qid then validation type, such as 'value_range', and 'num_answers'
*
* @example [703] = array(
* 'eqn' => array(
* 'value_range' = "((is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK >= (0)) and (is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK <= (5)))"
* ),
* 'tips' => array(
* 'value_range' = "Each answer must be between {fixnum(0)} and {fixnum(5)}"
* ),
* 'subqValidEqns' = array(
* [] = array(
* 'subqValidSelector' => '' //
* 'subqValidEqn' => "(is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK >= (0)) && (is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK <= (5))"
* ),
* 'sumEqn' => '' // the equation to compute the current sum of the responses
* 'sumRemainingEqn' => '' // the equation to how much is left (for the question attribute that lets you specify the exact value of the sum of the answers)
* );
*
* @var array
*/
private $qid2validationEqn;
/**
* keeps relevance in proper sequence so can minimize relevance processing to see what should be see on page and in indexes
* Array is indexed on qseq
*
* @example [3] = array(
* 'relevance' => "!is_empty(num)" // the question-level relevance equation
* 'grelevance' => "" // the group-level relevance equation
* 'qid' => "699" // the question id
* 'qseq' => 3 // the 0-index question sequence
* 'gseq' => 0 // the 0-index group sequence
* 'jsResultVar_on' => 'answer26626X34X699' // the javascript variable holding the input value
* 'jsResultVar' => 'java26226X34X699' // the javascript variable (often hidden) holding the value to be submitted
* 'type' => 'N' // the one character question type
* 'hidden' => 0 // 1 if it should be always_hidden
* 'gid' => "34" // group id
* 'mandatory' => 'N' // 'Y' if mandatory
* 'eqn' => "" // TODO ??
* 'help' => "" // the help text
* 'qtext' => "Enter a larger number than {num}" // the question text
* 'code' => 'afDS_sq5_1' // the full variable name
* 'other' => 'N' // whether the question supports the 'other' option - 'Y' if true
* 'rowdivid' => '2626X37X705sq5' // the javascript id for the row - in this case, the 5th subquestion
* 'aid' => 'sq5' // the answer id
* 'sqid' => '791' // the subquestion's qid (only populated for some question types)
* );
*
* @var array
*/
private $questionSeq2relevance;
/**
* current Group sequence (0-based index)
* @example 1
* @var integer
*/
private $currentGroupSeq;
/**
* for Question-by-Question mode, the 0-based index
* @example 3
* @var integer
*/
private $currentQuestionSeq;
/**
* used in Question-by-Question mode
* @var integer
*/
private $currentQID;
/**
* set of the current set of questions to be displayed, indexed by QID - at least one must be relevant
*
* The array has N entries, where N is the number if qids in the Qset. Each has the following contents:
* @example [705] = array(
* 'info' => array() // this is an exact copy of $questionSeq2relevance[$qseq] -- TODO - remove redundancy
* 'relevant' => 1 // 1 if the question is currently relevant
* 'hidden' => 0 // 1 if the question is always hidden
* 'relEqn' => '' // the relevance equation -- TODO - how different from ['info']['relevance']?
* 'sgqa' => // pipe-separated list of SGQA codes for this question -- e.g. "26626X37X705sq1#0|26626X37X705sq1#1|26626X37X705sq2#0|26626X37X705sq2#1|26626X37X705sq3#0|26626X37X705sq3#1|26626X37X705sq4#0|26626X37X705sq4#1|26626X37X705sq5#0|26626X37X705sq5#1"
* 'unansweredSQs' => // pipe-separated list of currently unanswered SGQA codes for this question -- e.g. "26626X37X705sq1#0|26626X37X705sq1#1|26626X37X705sq3#0|26626X37X705sq3#1|26626X37X705sq5#0|26626X37X705sq5#1"
* 'valid' => 0 // 1 if the current answers pass all of the validation criteria for the question
* 'validEqn' => // the auto-generated validation criteria, based upon advanced question attributes -- e.g. "((count(if(count(26626X37X705sq1#0.NAOK,26626X37X705sq1#1.NAOK)==2,1,''), if(count(26626X37X705sq2#0.NAOK,26626X37X705sq2#1.NAOK)==2,1,''), if(count(26626X37X705sq3#0.NAOK,26626X37X705sq3#1.NAOK)==2,1,''), if(count(26626X37X705sq4#0.NAOK,26626X37X705sq4#1.NAOK)==2,1,''), if(count(26626X37X705sq5#0.NAOK,26626X37X705sq5#1.NAOK)==2,1,'')) >= (minSelect)) and (count(if(count(26626X37X705sq1#0.NAOK,26626X37X705sq1#1.NAOK)==2,1,''), if(count(26626X37X705sq2#0.NAOK,26626X37X705sq2#1.NAOK)==2,1,''), if(count(26626X37X705sq3#0.NAOK,26626X37X705sq3#1.NAOK)==2,1,''), if(count(26626X37X705sq4#0.NAOK,26626X37X705sq4#1.NAOK)==2,1,''), if(count(26626X37X705sq5#0.NAOK,26626X37X705sq5#1.NAOK)==2,1,'')) <= (maxSelect)))"
* 'prettyValidEqn' => // syntax-highlighted version of validEqn, only showing syntax errors
* 'validTip' => // html fragment to insert for the validation tip -- e.g. "<div id='vmsg_705_num_answers' class='em_num_answers'>Please select between 1 and 3 answer(s)</div>"
* 'prettyValidTip' => // version of validTip that can be parsed by EM to create dynmamic validation -- e.g. "<div id='vmsg_705_num_answers' class='em_num_answers'>Please select between {fixnum(minSelect)} and {fixnum(maxSelect)} answer(s)</div>"
* 'validJS' => // JavaScript fragment that can perform validation. This is the result of parsing validEqn -- e.g. "LEMif(LEManyNA('minSelect', 'maxSelect'),'',(((LEMcount(LEMif(LEMcount(LEMval('26626X37X705sq1#0.NAOK') , LEMval('26626X37X705sq1#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq2#0.NAOK') , LEMval('26626X37X705sq2#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq3#0.NAOK') , LEMval('26626X37X705sq3#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq4#0.NAOK') , LEMval('26626X37X705sq4#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq5#0.NAOK') , LEMval('26626X37X705sq5#1.NAOK') ) == 2, 1, '')) >= (LEMval('minSelect') )) && (LEMcount(LEMif(LEMcount(LEMval('26626X37X705sq1#0.NAOK') , LEMval('26626X37X705sq1#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq2#0.NAOK') , LEMval('26626X37X705sq2#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq3#0.NAOK') , LEMval('26626X37X705sq3#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq4#0.NAOK') , LEMval('26626X37X705sq4#1.NAOK') ) == 2, 1, ''), LEMif(LEMcount(LEMval('26626X37X705sq5#0.NAOK') , LEMval('26626X37X705sq5#1.NAOK') ) == 2, 1, '')) <= (LEMval('maxSelect') )))))"
* 'invalidSQs' => // current list of subquestions that fail validation criteria -- e.g. "26626X37X705sq1#0|26626X37X705sq1#1|26626X37X705sq2#0|26626X37X705sq2#1|26626X37X705sq3#0|26626X37X705sq3#1|26626X37X705sq4#0|26626X37X705sq4#1|26626X37X705sq5#0|26626X37X705sq5#1"
* 'relevantSQs' => // current list of subquestions that are relevant -- e.g. "26626X37X705sq1#0|26626X37X705sq1#1|26626X37X705sq2#0|26626X37X705sq2#1|26626X37X705sq3#0|26626X37X705sq3#1|26626X37X705sq4#0|26626X37X705sq4#1|26626X37X705sq5#0|26626X37X705sq5#1"
* 'irrelevantSQs' => // current list of subquestions that are irrelevant -- e.g. "26626X37X705sq2#0|26626X37X705sq2#1|26626X37X705sq4#0|26626X37X705sq4#1"
* 'subQrelEqn' => // TODO - ??
* 'mandViolation' => 0 // 1 if the question is mandatory and fails the mandatory criteria
* 'anyUnanswered' => 1 // 1 if any parts of the question are unanswered
* 'mandTip' => '' // message to display if the question fails mandatory criteria
* 'message' => '' // TODO ??
* 'updatedValues' => // array of values that should be updated for this question, as [$sgqa] = $value
* 'sumEqn' => '' //
* 'sumRemainingEqn' => '' //
* );
*
* @var array|null
*/
private $currentQset=NULL;
/**
* last result of NavigateForwards, NavigateBackwards, or JumpTo
* Array of status information about last movement, whether at question, group, or survey level
*
* @example = array(
* 'finished' => 0 // 1 if the survey has been completed and needs to be finalized
* 'message' => '' // any error message that needs to be displayed
* 'seq' => 1 // the sequence count, using gseq, or qseq units if in 'group' or 'question' mode, respectively
* 'mandViolation' => 0 // whether there was any violation of mandatory constraints in the last movement
* 'valid' => 0 // 1 if the last movement passed all validation constraints. 0 if there were any validation errors
* 'unansweredSQs' => // pipe-separated list of any subquestions that were not answered
* 'invalidSQs' => // pipe-separated list of any subquestions that failed validation constraints
* );
*
* @var array|null
*/
private $lastMoveResult=NULL;
/**
* array of information needed to generate navigation index in question-by-question mode
* One entry for each question, indexed by qseq
*
* @example [4] = array(
* 'qid' => "700" // the question id
* 'qtext' => 'How old are you?' // the question text
* 'qcode' => 'age' // the variable name
* 'qhelp' => '' // the help text
* 'anyUnanswered' => 0 // 1 if there are any subquestions answered. Used for index display
* 'anyErrors' => 0 // 1 if there are any errors among the subquestions. Could be used for index display
* 'show' => 1 // 1 if there are any relevant, non-hidden subquestions. Only if so, then display the index entry
* 'gseq' => 0 // the group sequence
* 'gtext' => // text description for the group
* 'gname' => 'G1' // the group title
* 'gid' => "34" // the group id
* 'mandViolation' => 0 // 1 if the question as a whole fails the mandatory criteria
* 'valid' => 1 // 0 if any part of the question fails validation criteria.
* );
*
* @var array
*/
private $indexQseq;
/**
* array of information needed to generate navigation index in group-by-group mode
* One entry for each group, indexed by gseq
*
* @example [0] = array(
* 'gtext' => // the description for the group
* 'gname' => 'G1' // the group title
* 'gid' => '34' // the group id
* 'anyUnanswered' => 0 // 1 if any questions within the group are unanswered
* 'anyErrors' => 0 // 1 if any of the questions within the group fail either validity or mandatory constraints
* 'valid' => 1 // 1 if at least question in the group is relevant and non-hidden
* 'mandViolation' => 0 // 1 if at least one relevant, non-hidden question in the group fails mandatory constraints
* 'show' => 1 // 1 if there is at least one relevant, non-hidden question within the group
* );
*
* @var array
*/
private $indexGseq;
/**
* array of group sequence number to static info
* One entry per group, indexed on gseq
*
* @example [0] = array(
* 'group_order' => 0 // gseq
* 'gid' => "34" // group id
* 'group_name' => 'G2' // the group title
* 'description' => // the description of the group (e.g. gtitle)
* 'grelevance' => '' // the group-level relevance
* );
*
* @var array
*/
private $gseq2info;
/**
* the maximum groupSeq reached - this is needed for Index
* @var int
*/
private $maxGroupSeq;
/**
* the maximum Question reached sequencly ordered, used to show error to the user if we stop before this step with indexed survey.
* In question by question mode : $maxQuestionSeq==$_SESSION['survey_'.surveyid]['maxstep'], use it ?
* @var integer
*/
private $maxQuestionSeq=-1;
/**
/**
* mapping of questions to information about their subquestions.
* One entry per question, indexed on qid
*
* @example [702] = array(
* 'qid' => 702 // the question id
* 'qseq' => 6 // the question sequence
* 'gseq' => 0 // the group sequence
* 'sgqa' => '26626X34X702' // the root of the SGQA code (reallly just the SGQ)
* 'varName' => 'afSrcFilter_sq1' // the full qcode variable name - note, if there are subquestions, 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
* 'preg' => '/[A-Z]+/' // regular expression validation equation, if any
* 'subqs' => array() of subquestions, 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 subquestion
* 'jsVarName_on' => 'java26626X34X702sq1' // the JavaScript variable name if the variable is defined on the current page
* 'jsVarName' => 'java26626X34X702sq1' // the JavaScript variable name to use if the variable is defined on a different page
* 'csuffix' => 'sq1' // the SGQ suffix to use for a fieldname
* 'sqsuffix' => '_sq1' // the suffix to use for a qcode variable name
* );
*
* @var array
*/
private $q2subqInfo;
/**
* array of advanced question attributes for each question
* Indexed by qid; available for all quetions
*
* @example [784] = array(
* 'array_filter_exclude' => 'afSrcFilter'
* 'exclude_all_others' => 'sq5'
* 'max_answers' => '3'
* 'min_answers' => '1'
* 'other_replace_text' => '{afSrcFilter_other}'
* );
*
* @var array
*/
private $qattr;
/**
* list of needed subquestion relevance (e.g. array_filter)
* Indexed by qid then sgqa; only generated for current group of questions
*
* @example [708][26626X37X708sq2] = array(
* 'qid' => '708' // the question id
* 'eqn' => "((26626X34X702sq2 != ''))" // the auto-generated subquestion-level relevance equation
* 'prettyPrintEqn' => '' // only generated if there errors - shows syntax highlighting of them
* 'result' => 0 // result of processing the subquestion-level relevance equation in the current context
* 'numJsVars' => 1 // the number of on-page javascript variables in 'eqn'
* 'relevancejs' => // the generated javascript from 'eqn' -- e.g. "LEMif(LEManyNA('26626X34X702sq2'),'',(((LEMval('26626X34X702sq2') != ""))))"
* 'relevanceVars' => "java26626X34X702sq2" // the pipe-separated list of on-page javascript variables in 'eqn'
* 'rowdivid' => "26626X37X708sq2" // the javascript id of the question row (so can apply array_filter)
* 'type' => 'array_filter' // semicolon delimited list of types of subquestion relevance filters applied
* 'qtype' => 'A' // the single character question type
* 'sgqa' => "26626X37X708" // the SGQ portion of the fieldname
* 'hasErrors' => 0 // 1 if there are any parse errors in the subquestion validation equations
* );
*
* @var array
*/
private $subQrelInfo=array();
/**
* array of Group-level relevance status
* Indexed by gseq; only shows groups that have been visited
*
* @example [1] = array(
* 'gseq' => 1 // group sequence
* 'eqn' => '' // the group-level relevance
* 'result' => 1 // result of processing the group-level relevance
* 'numJsVars' => 0 // the number of on-page javascript variables in the group-level relevance equation
* 'relevancejs' => '' // the javascript version of the relevance equation
* 'relevanceVars' => '' // the pipe-delimited list of on-page javascript variable names used within the group-level relevance equation
* 'prettyPrint' => '' // a pretty-print version of the group-level relevance equation, only if there are errors
* );
*
* @var array
*/
private $gRelInfo=array();
/**
* Array of timing information to debug how long it takes for portions of LEM to run.
* Array of timing information (in seconds) for EM to help with debugging
*
* @example [1] = array(
* [0]="LimeExpressionManager::NavigateForwards"
* [1]=1.7079849243164
* );
*
* @var array
*/
private $runtimeTimings=array();
/**
* True (1) if calling LimeExpressionManager functions between StartSurvey and FinishProcessingPage
* Used (mostly deprecated) to detect calls to LEM which happen outside of the normal processing scope
* @var boolean
*/
private $initialized=false;
/**
* True (1) if have already processed the relevance equations (so don't need to do it again)
*
* @var boolean
*/
private $processedRelevance=false;
/**
* Message generated to show debug timing values, if debugLevel includes LEM_DEBUG_TIMING
* @var string
*/
private $debugTimingMsg='';
/**
* temporary variable to reduce need to parse same equation multiple times. Used for relevance and validation
* Array, indexed on equation, providing the following information:
*
* @example ['!is_empty(num)'] = array(
* 'result' => 1 // result of processing the equation in the current scope
* 'prettyPrint' => '' // syntax-highlighted version of equation if there are any errors
* 'hasErrors' => 0 // 1 if there are any syntax errors
* );
*
* @var array
*/
private $ParseResultCache;
/**
* array of 2nd scale answer lists for types ':' and ';' -- needed for convenient print of logic file
* Indexed on qid; available for all questions
*
* @example [706] = array(
* '1~1' => '1|Never',
* '1~2' => '2|Sometimes',
* '1~3' => '3|Always'
* );
*
* @var array
*/
private $multiflexiAnswers;
/**
* used to specify whether to generate equations using SGQA codes or qcodes
* Default is to convert all qcode naming to sgqa naming when generating javascript, as that provides the greatest backwards compatibility
* TSV export of survey structure sets this to false so as to force use of qcode naming
*
* @var Boolean
*/
private $sgqaNaming = true;
/**
* Number of groups in survey (number of possible pages to display)
* @var integer
*/
private $numGroups=0;
/**
* Numer of questions in survey (counting display-only ones?)
* @var integer
*/
private $numQuestions=0;
/**
* String identifier for the active session
* @var string
*/
private $sessid;
/**
* Linked list of array filters
* @var array
*/
private $qrootVarName2arrayFilter = array();
/**
* Array, keyed on qid, to JavaScript and list of variables needed to implement exclude_all_others_auto
* @var array
*/
private $qid2exclusiveAuto = array();
/**
* Array of invalid answer, key is sgq, value is the clear string to be shown
* Must be always unset after using (EM are in $_SESSION and never new ....)
*
* @var string[]
*/
private $invalidAnswerString = array();
/**
* Array of values to be updated
* @var array
*/
private $updatedValues = array();
/**
* A private constructor; prevents direct creation of object
*/
private function __construct()
{
self::$instance =& $this;
$this->em = new ExpressionManager();
if (!isset($_SESSION['LEMlang'])) {
$_SESSION['LEMlang'] = 'en'; // so that there is a default
}
}
/**
* Ensures there is only one instances of LEM. Note, if switch between surveys, have to clear this cache
* @return LimeExpressionManager
*/
public static function &singleton()
{
$now = microtime(true);
if (isset($_SESSION['LEMdirtyFlag'])) {
$c = __CLASS__;
self::$instance = new $c;
unset($_SESSION['LEMdirtyFlag']);
}
else if (!isset(self::$instance)) {
if (isset($_SESSION['LEMsingleton'])) {
self::$instance = unserialize($_SESSION['LEMsingleton']);
}
else {
$c = __CLASS__;
self::$instance = new $c;
}
}
else {
// does exist, and OK to cache
return self::$instance;
}
// only record duration if have to create (or unserialize) an instance
self::$instance->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
return self::$instance;
}
/**
* Prevent users to clone the instance
*/
public function __clone()
{
trigger_error('Clone is not allowed.', E_USER_ERROR);
}
/**
* Set the previewmode
* @param string|false $previewmode 'question', 'group', false
* @return void
*/
public static function SetPreviewMode($previewmode=false)
{
$LEM =& LimeExpressionManager::singleton();
$LEM->sPreviewMode=$previewmode;
}
/**
* Tells Expression Manager that something has changed enough that needs to eliminate internal caching
* @return void
*/
public static function SetDirtyFlag()
{
$_SESSION['LEMdirtyFlag'] = true;// For fieldmap and other. question help {HELP} is taken from fieldmap
$_SESSION['LEMforceRefresh'] = true;// For Expression manager string
/* Bug #09589 : update a survey don't reset actual test => Force reloading of survey */
$iSessionSurveyId=self::getLEMsurveyId();
if($aSessionSurvey=Yii::app()->session["survey_{$iSessionSurveyId}"])
{
$aSessionSurvey['LEMtokenResume']=true;
Yii::app()->session["survey_{$iSessionSurveyId}"]=$aSessionSurvey;
}
}
/**
* Set the SurveyId - really checks whether the survey you're about to work with is new, and if so, clears the LEM cache
* @param integer|null $sid
*/
public static function SetSurveyId($sid=NULL)
{
if (!is_null($sid)) {
if (isset($_SESSION['LEMsid']) && $sid != $_SESSION['LEMsid']) {
// then trying to use a new survey - so clear the LEM cache
self::SetDirtyFlag();
}
$_SESSION['LEMsid'] = $sid;
}
}
/**
* Sets the language for Expression Manager. If the language has changed, then EM cache must be invalidated and refreshed
* @param string|null $lang
* @return void
*/
public static function SetEMLanguage($lang=NULL)
{
if (is_null($lang)) {
return; // should never happen
}
if (!isset($_SESSION['LEMlang'])) {
$_SESSION['LEMlang'] = $lang;
}
if ($_SESSION['LEMlang'] != $lang) {
// then changing languages, so clear cache
self::SetDirtyFlag();
}
$_SESSION['LEMlang'] = $lang;
}
/**
* Do bulk-update/save of Condition to Relevance
* @param integer|null $surveyId - if NULL, processes the entire database, otherwise just the specified survey
* @param integer|null $qid - if specified, just updates that one question
* @return array of query strings
*/
public static function UpgradeConditionsToRelevance($surveyId=NULL, $qid=NULL)
{
LimeExpressionManager::SetDirtyFlag(); // set dirty flag even if not conditions, since must have had a DB change
// Cheat and upgrade question attributes here too.
self::UpgradeQuestionAttributes(true,$surveyId,$qid);
if (is_null($surveyId))
{
$sQuery='SELECT sid FROM {{surveys}}';
$aSurveyIDs = Yii::app()->db->createCommand($sQuery)->queryColumn();
}
else{
$aSurveyIDs=array($surveyId);
}
foreach ($aSurveyIDs as $surveyId ) {
// echo $surveyId.'<br>';flush();@ob_flush();
$releqns = self::ConvertConditionsToRelevance($surveyId,$qid);
if ( !empty( $releqns) ) {
foreach ($releqns as $key=>$value)
{
$sQuery = "UPDATE {{questions}} SET relevance=".Yii::app()->db->quoteValue($value)." WHERE qid=".$key;
Yii::app()->db->createCommand($sQuery)->execute();
}
}
}
LimeExpressionManager::SetDirtyFlag();
}
/**
* This reverses UpgradeConditionsToRelevance(). It removes Relevance for questions that have Condition
* @param integer|null $surveyId
* @param integer|null $qid
* @return int
*/
public static function RevertUpgradeConditionsToRelevance($surveyId=NULL, $qid=NULL)
{
LimeExpressionManager::SetDirtyFlag(); // set dirty flag even if not conditions, since must have had a DB change
$releqns = self::ConvertConditionsToRelevance($surveyId,$qid);
if(!is_array($releqns)) {
return NULL;
}
$num = count($releqns);
if ($num == 0) {
return NULL;
}
foreach ($releqns as $key=>$value) {
$query = "UPDATE {{questions}} SET relevance=1 WHERE qid=".$key;
//dbExecuteAssoc($query);
$data = Yii::app()->db->createCommand($query)->query();
}
return count($releqns);
}
/**
* Return array database name as key, LEM name as value
* @example (['gender'] => '38612X10X145')
* @param integer $iSurveyId
* @return array
**/
public static function getLEMqcode2sgqa($iSurveyId){
$LEM =& LimeExpressionManager::singleton();
$LEM->SetSurveyId($iSurveyId); // This update session only if needed
if( !in_array(Yii::app()->session['LEMlang'],Survey::model()->findByPk($iSurveyId)->getAllLanguages()) ) {
$LEM->SetEMLanguage(Survey::model()->findByPk($iSurveyId)->language);// Reset language only if needed
}
$LEM->setVariableAndTokenMappingsForExpressionManager($iSurveyId);
return $LEM->qcode2sgqa;
}
/**
* If $qid is set, returns the relevance equation generated from conditions (or NULL if there are no conditions for that $qid)
* If $qid is NULL, returns an array of relevance equations generated from Condition, keyed on the question ID
* @param integer|null $surveyId
* @param integer|null $qid - if passed, only generates relevance equation for that question - otherwise genereates for all questions with conditions
* @return array of generated relevance strings, indexed by $qid
*/
public static function ConvertConditionsToRelevance($surveyId=NULL, $qid=NULL)
{
$query = LimeExpressionManager::getConditionsForEM($surveyId,$qid);
$aConditions=$query->readAll();
$_qid = -1;
$_subqid = -1;
$_cqid = 0;
$_scenario = 0;
$relevanceEqns = array();
$scenarios = array();
$relAndList = array();
$relOrList = array();
foreach($aConditions as $row)
{
$row['method']=trim($row['method']); //For Postgres
if ($row['qid'] != $_qid)
{
// output the values for prior question is there was one
if ($_qid != -1)
{
if (count($relOrList) > 0)
{
$relAndList[] = '(' . implode(' or ', $relOrList) . ')';
}
if (count($relAndList) > 0)
{
$scenarios[] = '(' . implode(' and ', $relAndList) . ')';
}
$relevanceEqn = implode(' or ', $scenarios);
$relevanceEqns[$_qid] = $relevanceEqn;
}
// clear for next question
$_qid = $row['qid'];
$_scenario = $row['scenario'];
$_cqid = $row['cqid'];
$_subqid = -1;
$relAndList = array();
$relOrList = array();
$scenarios = array();
}
if ($row['scenario'] != $_scenario)
{
if (count($relOrList) > 0)
{
$relAndList[] = '(' . implode(' or ', $relOrList) . ')';
}
$scenarios[] = '(' . implode(' and ', $relAndList) . ')';
$relAndList = array();
$relOrList = array();
$_scenario = $row['scenario'];
$_cqid = $row['cqid'];
$_subqid = -1;
}
if ($row['cqid'] != $_cqid)
{
$relAndList[] = '(' . implode(' or ', $relOrList) . ')';
$relOrList = array();
$_cqid = $row['cqid'];
$_subqid = -1;
}
// fix fieldnames
if ($row['type'] == '' && preg_match('/^{.+}$/',$row['cfieldname'])) {
$fieldname = (string)substr($row['cfieldname'],1,-1); // {TOKEN:xxxx}
$subqid = $fieldname;
$value = $row['value'];
}
else if ($row['type'] == 'M' || $row['type'] == 'P') {
if ((string)substr($row['cfieldname'],0,1) == '+') {
// if prefixed with +, then a fully resolved name
$fieldname = (string)substr($row['cfieldname'],1) . '.NAOK';
$subqid = $fieldname;
$value = $row['value'];
}
else {
// else create name by concatenating two parts together
$fieldname = $row['cfieldname'] . $row['value'] . '.NAOK';
$subqid = $row['cfieldname'];
$value = 'Y';
}
}
else {
$fieldname = $row['cfieldname'] . '.NAOK';
$subqid = $fieldname;
$value = $row['value'];
}
if ($_subqid != -1 && $_subqid != $subqid)
{
$relAndList[] = '(' . implode(' or ', $relOrList) . ')';
$relOrList = array();
}
$_subqid = $subqid;
if (preg_match('/^@\d+X\d+X\d+.*@$/',$value)) {
$value = (string)substr($value,1,-1);
} elseif (preg_match('/^{.+}$/',$value)) {
$value = (string)substr($value,1,-1);
} elseif ($row['method'] == 'RX') {
if (!preg_match('#^/.*/$#',$value)) {
$value = '"/' . $value . '/"'; // if not surrounded by slashes, add them.
}
} elseif ((string)(float) $value !== (string) $value ) {
$value = '"' . $value . '"';
}
// add equation
if ($row['method'] == 'RX')
{
$relOrList[] = "regexMatch(" . $value . "," . $fieldname . ")";
}
else
{
// Condition uses ' ' to mean not answered, but internally it is really stored as ''. Fix this
if ($value === '" "' || $value == '""') {
if ($row['method'] == '==')
{
$relOrList[] = "is_empty(" . $fieldname . ")";
}
else if ($row['method'] == '!=')
{
$relOrList[] = "!is_empty(" . $fieldname . ")";
}
else
{
$relOrList[] = $fieldname . " " . $row['method'] . " " . $value;
}
}
else
{
if ($value == '"0"' || !preg_match('/^".+"$/',$value))
{
switch ($row['method'])
{
case '==':
case '<':
case '<=':
case '>=':
$relOrList[] = '(!is_empty(' . $fieldname . ') && (' . $fieldname . " " . $row['method'] . " " . $value . '))';
break;
case '!=':
$relOrList[] = '(is_empty(' . $fieldname . ') || (' . $fieldname . " != " . $value . '))';
break;
default:
$relOrList[] = $fieldname . " " . $row['method'] . " " . $value;
break;
}
}
else
{
switch ($row['method'])
{
case '<':
case '<=':
$relOrList[] = '(!is_empty(' . $fieldname . ') && (' . $fieldname . " " . $row['method'] . " " . $value . '))';
break;
default:
$relOrList[] = $fieldname . " " . $row['method'] . " " . $value;
break;
}
}
}
}
if (($row['cqid'] == 0 && preg_match('/^{TOKEN:([^}]*)}$/',$row['cfieldname']) && preg_match('/^{TOKEN:([^}]*)}$/',isset($previousCondition)?$previousCondition['cfieldname']:'')) || substr($row['cfieldname'],0,1) == '+') {
$_cqid = -1; // forces this statement to be ANDed instead of being part of a cqid OR group (except for TOKEN fields that follow a a token field)
}
$previousCondition=$row;
}
// output last one
if ($_qid != -1)
{
if (count($relOrList) > 0)
{
$relAndList[] = '(' . implode(' or ', $relOrList) . ')';
}
if (count($relAndList) > 0)
{
$scenarios[] = '(' . implode(' and ', $relAndList) . ')';
}
$relevanceEqn = implode(' or ', $scenarios);
$relevanceEqns[$_qid] = $relevanceEqn;
}
if (is_null($qid)) {
return $relevanceEqns;
}
else {
if (isset($relevanceEqns[$qid]))
{
$result = array();
$result[$qid] = $relevanceEqns[$qid];
return $result;
}
else
{
return NULL;
}
}
}
/**
* Return list of relevance equations generated from conditions
* @param integer|null $surveyId
* @param integer|null $qid
* @return array of relevance equations, indexed by $qid
*/
public static function UnitTestConvertConditionsToRelevance($surveyId=NULL, $qid=NULL)
{
$LEM =& LimeExpressionManager::singleton();
return $LEM->ConvertConditionsToRelevance($surveyId, $qid);
}
/**
* Process all question attributes that apply to EM
* (1) subquestion-level relevance: e.g. array_filter, array_filter_exclude, relevance equations entered in SQ-mask
* (2) Validations: e.g. min/max number of answers; min/max/eq sum of answers
* @param integer|null $onlyThisQseq - only process these attributes for the specified question
* @return void
*/
public function _CreateSubQLevelRelevanceAndValidationEqns($onlyThisQseq=NULL)
{
// $now = microtime(true);
$this->subQrelInfo=array(); // reset it each time this is called
$subQrels = array(); // array of subquestion-level relevance equations
$validationEqn = array();
$validationTips = array(); // array of visible tips for validation criteria, indexed by $qid
// Associate these with $qid so that can be nested under appropriate question-level relevance
foreach ($this->q2subqInfo as $qinfo)
{
if (!is_null($onlyThisQseq) && $onlyThisQseq != $qinfo['qseq']) {
continue;
}
else if (!$this->allOnOnePage && $this->currentGroupSeq != $qinfo['gseq']) {
continue; // only need subq relevance for current page.
}
$questionNum = $qinfo['qid'];
$type = $qinfo['type'];
$hasSubqs = (isset($qinfo['subqs']) && count($qinfo['subqs']) > 0);
$qattr = isset($this->qattr[$questionNum]) ? $this->qattr[$questionNum] : array();
if (isset($qattr['value_range_allows_missing']) && $qattr['value_range_allows_missing'] == '1')
{
$value_range_allows_missing = true;
}
else
{
$value_range_allows_missing = false;
}
// 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'];
$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'];
$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) {
list($cascadedAF, $cascadedAFE) = $this->_recursivelyFindAntecdentArrayFilters($qinfo['rootVarName'],array(),array());
$cascadedAF = array_reverse($cascadedAF);
$cascadedAFE = array_reverse($cascadedAFE);
$subqs = $qinfo['subqs'];
if ($type == 'R') {
$subqs = array();
foreach ($this->qans[$qinfo['qid']] as $k=>$v)
{
$_code = explode('~',$k);
$subqs[] = array(
'rowdivid'=>$qinfo['sgqa'] . $_code[1],
'sqsuffix'=>'_' . $_code[1],
);
}
}
$last_rowdivid = '--';
foreach ($subqs as $sq) {
if ($sq['rowdivid'] == $last_rowdivid)
{
continue;
}
$last_rowdivid = $sq['rowdivid'];
$af_names = array();
$afe_names = array();
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
case 'R': //Ranking
// if ($this->sgqaNaming)
// {
foreach ($cascadedAF as $_caf)
{
$sgq = ((isset($this->qcode2sgq[$_caf])) ? $this->qcode2sgq[$_caf] : $_caf);
$fqid = explode('X',$sgq);
if (!isset($fqid[2]))
{
continue;
}
$fqid = $fqid[2];
if ($this->q2subqInfo[$fqid]['type'] == 'R')
{
$rankables = array();
foreach ($this->qans[$fqid] as $k=>$v)
{
$rankable = explode('~',$k);
$rankables[] = '_' . $rankable[1];
}
if (array_search($sq['sqsuffix'],$rankables) === false)
{
continue;
}
}
$fsqs = array();
foreach ($this->q2subqInfo[$fqid]['subqs'] as $fsq)
{
if (!isset($fsq['csuffix'])) $fsq['csuffix']='';
if ($this->q2subqInfo[$fqid]['type'] == 'R')
{
// we know the suffix exists
$fsqs[] = '(' . $sgq . $fsq['csuffix'] . ".NAOK == '" . (string)substr($sq['sqsuffix'],1) . "')";
}
else if ($this->q2subqInfo[$fqid]['type'] == ':' && isset($this->qattr[$fqid]['multiflexible_checkbox']) && $this->qattr[$fqid]['multiflexible_checkbox']=='1')
{
if ($fsq['sqsuffix'] == $sq['sqsuffix'])
{
$fsqs[] = $sgq . $fsq['csuffix'] . '.NAOK=="1"';
}
}
else
{
if ($fsq['sqsuffix'] == $sq['sqsuffix'])
{
$fsqs[] = '!is_empty(' . $sgq . $fsq['csuffix'] . '.NAOK)';
}
}
}
if (count($fsqs) > 0)
{
$af_names[] = '(' . implode(' or ', $fsqs) . ')';
}
}
foreach ($cascadedAFE as $_cafe)
{
$sgq = ((isset($this->qcode2sgq[$_cafe])) ? $this->qcode2sgq[$_cafe] : $_cafe);
$fqid = explode('X',$sgq);
if (!isset($fqid[2]))
{
continue;
}
$fqid = $fqid[2];
if ($this->q2subqInfo[$fqid]['type'] == 'R')
{
$rankables = array();
foreach ($this->qans[$fqid] as $k=>$v)
{
$rankable = explode('~',$k);
$rankables[] = '_' . $rankable[1];
}
if (array_search($sq['sqsuffix'],$rankables) === false)
{
continue;
}
}
$fsqs = array();
foreach ($this->q2subqInfo[$fqid]['subqs'] as $fsq)
{
if ($this->q2subqInfo[$fqid]['type'] == 'R')
{
// we know the suffix exists
$fsqs[] = '(' . $sgq . $fsq['csuffix'] . ".NAOK != '" . substr($sq['sqsuffix'],1) . "')";
}
else if ($this->q2subqInfo[$fqid]['type'] == ':' && isset($this->qattr[$fqid]['multiflexible_checkbox']) && $this->qattr[$fqid]['multiflexible_checkbox']=='1')
{
if ($fsq['sqsuffix'] == $sq['sqsuffix'])
{
$fsqs[] = $sgq . $fsq['csuffix'] . '.NAOK!="1"';
}
}
else
{
if ($fsq['sqsuffix'] == $sq['sqsuffix'])
{
$fsqs[] = 'is_empty(' . $sgq . $fsq['csuffix'] . '.NAOK)';
}
}
}
if (count($fsqs) > 0)
{
$afe_names[] = '(' . implode(' and ', $fsqs) . ')';
}
}
// }
// else // TODO - implement qcode naming for this
// {
// foreach ($cascadedAF as $_caf)
// {
// $sgq = $_caf . $sq['sqsuffix'];
// if (isset($this->knownVars[$sgq]))
// {
// $af_names[] = $sgq . '.NAOK';
// }
// }
// foreach ($cascadedAFE as $_cafe)
// {
// $sgq = $_cafe . $sq['sqsuffix'];
// if (isset($this->knownVars[$sgq]))
// {
// $afe_names[] = $sgq . '.NAOK';
// }
// }
// }
break;
default:
break;
}
$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(' && ', $afe_names);
}
$subQrels[] = array(
'qtype' => $type,
'type' => 'array_filter',
'rowdivid' => $sq['rowdivid'],
'eqn' => '(' . $afs_eqn . ')',
'qid' => $questionNum,
'sgqa' => $qinfo['sgqa'],
);
}
}
}
}
// individual subquestion relevance
if ($hasSubqs &&
$type!='|' && $type!='!' && $type !='L' && $type !='O'
)
{
$subqs = $qinfo['subqs'];
$last_rowdivid = '--';
foreach ($subqs as $sq)
{
if ($sq['rowdivid'] == $last_rowdivid)
{
continue;
}
$last_rowdivid = $sq['rowdivid'];
$rowdivid=$sq['rowdivid'];
switch($type)
{
case '1': //Array (Flexible Labels) dual scale
$rowdivid = $rowdivid . '#0';
break;
case ':': //ARRAY Numbers
case ';': //ARRAY Text
$aCsuffix=(explode('_',$sq['csuffix']));
$rowdivid = $rowdivid . '_'.$aCsuffix[1];
break;
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 'M': //Multiple choice checkbox
case 'P': //Multiple choice with comments checkbox + text
case 'K': //MULTIPLE NUMERICAL QUESTION
case 'Q': //MULTIPLE SHORT TEXT
break;
default:
break;
}
if (isset($this->knownVars[$rowdivid]['SQrelevance']) && $this->knownVars[$rowdivid]['SQrelevance']!='')
{
$subQrels[] = array(
'qtype' => $type,
'type' => 'SQ_relevance',
'rowdivid' => $sq['rowdivid'],
'eqn' => $this->knownVars[$rowdivid]['SQrelevance'],
'qid' => $questionNum,
'sgqa' => $qinfo['sgqa'],
);
}
}
}
// code_filter: WZ
// This can be skipped, since question types 'W' (list-dropdown-flexible) and 'Z'(list-radio-flexible) are no longer supported
// Default validation for question type
switch ($type)
{
case 'I':
case '!':
case 'O':
case 'M': //NUMERICAL QUESTION TYPE
case 'L': //LIST drop-down/radio-button list
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'default',
'class' => 'default',
'eqn' => '1',
'qid' => $questionNum,
);
break;
case 'N': //NUMERICAL QUESTION TYPE
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_equs=array();
foreach($subqs as $sq)
{
$sq_name = ($this->sgqaNaming)?$sq['rowdivid'].".NAOK":$sq['varName'].".NAOK";
$sq_equs[] = '( is_numeric('.$sq_name.') || is_empty('.$sq_name.') )';// Leave mandatory to mandatory attribute
if($type=="K")
$subqValidSelector = $sq['jsVarName_on'];
else
$subqValidSelector = "";
}
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'default',
'class' => 'default',
'eqn' => implode(' and ',$sq_equs),
'qid' => $questionNum,
);
}
break;
case 'K': //MULTI NUMERICAL QUESTION TYPE
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_equs=array();
$subqValidEqns = array();
foreach($subqs as $sq)
{
$sq_name = ($this->sgqaNaming)?$sq['rowdivid'].".NAOK":$sq['varName'].".NAOK";
$sq_equ = '( is_numeric('.$sq_name.') || is_empty('.$sq_name.') )';// Leave mandatory to mandatory attribute
$subqValidSelector = $sq['jsVarName_on'];
if (!is_null($sq_name)) {
$sq_equs[] = $sq_equ;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_equ,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'default',
'class' => 'default',
'eqn' => implode(' and ',$sq_equs),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
break;
case 'R':
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names=array();
$sq_eqPart=array();
foreach($subqs as $subq)
{
$sq_names[] = $subq['varName'].".NAOK";
$sq_eqPart[] = "intval(!is_empty({$subq['varName']}.NAOK))*{$subq['csuffix']}";
}
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'default',
'class' => 'default',
'eqn' => 'unique(' . implode(',',$sq_names) . ') and count(' . implode(',',$sq_names) . ')==max('. implode(',',$sq_eqPart) .')',
'qid' => $questionNum,
);
}
break;
case 'D':
// TODO: generic validation as to dateformat[SGQA].value : BUT not same in PHP and JS
break;
default:
break;
}
// commented_checkbox : only for checkbox with comment ("P")
$commented_checkbox='';
if (isset($qattr['commented_checkbox']) && trim($qattr['commented_checkbox']) != '')
{
switch ($type)
{
case 'P':
if ($hasSubqs) {
$commented_checkbox=$qattr['commented_checkbox'];
$subqs = $qinfo['subqs'];
$eqn='';
switch ($commented_checkbox)
{
case 'checked':
$sq_eqn_commented_checkbox=array();
foreach($subqs as $subq)
{
$sq_eqn_commented_checkbox[] = "(is_empty({$subq['varName']}.NAOK) and !is_empty({$subq['varName']}comment.NAOK))";
}
$eqn="sum(".implode(",",$sq_eqn_commented_checkbox).")==0";
break;
case 'unchecked':
$sq_eqn_commented_checkbox=array();
foreach($subqs as $subq)
{
$sq_eqn_commented_checkbox[] = "(!is_empty({$subq['varName']}.NAOK) and !is_empty({$subq['varName']}comment.NAOK))";
}
$eqn="sum(".implode(",",$sq_eqn_commented_checkbox).")==0";
break;
case 'allways':
default:
break;
}
if($commented_checkbox!="allways")
{
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'commented_checkbox',
'class' => 'commented_checkbox',
'eqn' => $eqn,
'qid' => $questionNum,
);
}
}
break;
default:
break;
}
}
// dropdown_dates
// dropdown box: validate that a complete date is entered
if (isset($qattr['dropdown_dates']) && $qattr['dropdown_dates'])
{
$dropdown_dates = $qattr['dropdown_dates'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'D': //DATE QUESTION TYPE
$sq_name = ($this->sgqaNaming)?$sq['rowdivid'].".NAOK":$sq['varName'].".NAOK";
$sq_name = '('.$sq_name.'!="INVALID")';
$sq_names[] = $sq_name;
//$subqValidSelector = '';
break;
default:
break;
}
// Commented out because it does not do anything because of $subqValidSelector being empty
// @todo: Test dropdown date question validation
/* if (!is_null($sq_name)) {
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_name,
'subqValidSelector' => $subqValidSelector,
);
}*/
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'dropdown_dates',
'class' => 'dropdown_dates',
'eqn' => implode(' && ', $sq_names),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$dropdown_dates='';
}
// date_min
// Maximum date allowed in date question
if (isset($qattr['date_min']) && trim($qattr['date_min']) != '')
{
$date_min = $qattr['date_min'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'D': //DATE QUESTION TYPE
// date_min: Determine whether we have an expression, a full date (YYYY-MM-DD) or only a year(YYYY)
if (trim($qattr['date_min'])!='')
{
$mindate=$qattr['date_min'];
if ((strlen($mindate)==4) && ($mindate>=1900) && ($mindate<=2099))
{
// backward compatibility: if only a year is given, add month and day
$date_min='\''.$mindate.'-01-01'.' 00:00\'';
}
elseif (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/",$mindate))
{
$date_min='\''.$mindate.' 00:00\'';
}
elseif (array_key_exists($date_min, $this->qcode2sgqa)) // refers to another question
{
$date_min=$date_min.'.NAOK';
}
}
$sq_name = ($this->sgqaNaming)?$sq['rowdivid'].".NAOK":$sq['varName'].".NAOK";
$sq_name = '(is_empty(' . $sq_name . ') || ('. $sq_name . ' >= date("Y-m-d H:i", strtotime(' . $date_min . ')) ))';
$subqValidSelector = '';
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_name,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'date_min',
'class' => 'value_range',
'eqn' => implode(' && ', $sq_names),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$date_min='';
}
// date_max
// Maximum date allowed in date question
if (isset($qattr['date_max']) && trim($qattr['date_max']) != '')
{
$date_max = $qattr['date_max'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'D': //DATE QUESTION TYPE
// date_max: Determine whether we have an expression, a full date (YYYY-MM-DD) or only a year(YYYY)
if (trim($qattr['date_max'])!='')
{
$maxdate=$qattr['date_max'];
if ((strlen($maxdate)==4) && ($maxdate>=1900) && ($maxdate<=2099))
{
// backward compatibility: if only a year is given, add month and day
$date_max='\''.$maxdate.'-12-31 23:59'.'\'';
}
elseif (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/",$maxdate))
{
$date_max='\''.$maxdate.' 23:59\'';
}
elseif (array_key_exists($date_max, $this->qcode2sgqa)) // refers to another question
{
$date_max=$date_max.'.NAOK';
}
}
$sq_name = ($this->sgqaNaming)?$sq['rowdivid'].".NAOK":$sq['varName'].".NAOK";
$sq_name = '(is_empty(' . $sq_name . ') || is_empty(' . $date_max . ') || ('. $sq_name . ' <= date("Y-m-d H:i", strtotime(' . $date_max . ')) ))';
$subqValidSelector = '';
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_name,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'date_max',
'class' => 'value_range',
'eqn' => implode(' && ', $sq_names),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$date_max='';
}
// equals_num_value
// Validation:= sum(sq1,...,sqN) == value (which could be an expression).
if (isset($qattr['equals_num_value']) && trim($qattr['equals_num_value']) != '')
{
$equals_num_value = $qattr['equals_num_value'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'K': //MULTIPLE NUMERICAL QUESTION
if ($this->sgqaNaming)
{
$sq_name = $sq['rowdivid'] . '.NAOK';
}
else
{
$sq_name = $sq['varName'] . '.NAOK';
}
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
// sumEqn and sumRemainingEqn may need to be rounded if using sliders
$precision=LEM_DEFAULT_PRECISION; // default is not to round
if (isset($qattr['slider_layout']) && $qattr['slider_layout']=='1')
{
$precision=0; // default is to round to whole numbers
if (isset($qattr['slider_accuracy']) && trim($qattr['slider_accuracy'])!='')
{
$slider_accuracy = $qattr['slider_accuracy'];
$_parts = explode('.',$slider_accuracy);
if (isset($_parts[1]))
{
$precision = strlen($_parts[1]); // number of digits after mantissa
}
}
}
$sumEqn = 'sum(' . implode(', ', $sq_names) . ')';
$sumRemainingEqn = '(' . $equals_num_value . ' - sum(' . implode(', ', $sq_names) . '))';
$mainEqn = 'sum(' . implode(', ', $sq_names) . ')';
if (!is_null($precision))
{
$sumEqn = 'round(' . $sumEqn . ', ' . $precision . ')';
$sumRemainingEqn = 'round(' . $sumRemainingEqn . ', ' . $precision . ')';
$mainEqn = 'round(' . $mainEqn . ', ' . $precision . ')';
}
$noanswer_option = '';
if ($value_range_allows_missing)
{
$noanswer_option = ' || count(' . implode(', ', $sq_names) . ') == 0';
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'equals_num_value',
'class' => 'sum_equals',
'eqn' => ($qinfo['mandatory']=='Y')?'(' . $mainEqn . ' == (' . $equals_num_value . '))':'(' . $mainEqn . ' == (' . $equals_num_value . ')' . $noanswer_option . ')',
'qid' => $questionNum,
'sumEqn' => $sumEqn,
'sumRemainingEqn' => $sumRemainingEqn,
);
}
}
}
else
{
$equals_num_value='';
}
// exclude_all_others
// If any excluded options are true (and relevant), then disable all other input elements for that question
if (isset($qattr['exclude_all_others']) && trim($qattr['exclude_all_others']) != '')
{
$exclusive_options = explode(';',$qattr['exclude_all_others']);
if ($hasSubqs) {
foreach ($exclusive_options as $exclusive_option)
{
$exclusive_option = trim($exclusive_option);
if ($exclusive_option == '') {
continue;
}
$subqs = $qinfo['subqs'];
foreach ($subqs as $sq) {
$sq_name = NULL;
if ($sq['csuffix'] == $exclusive_option)
{
continue; // so don't make the excluded option irrelevant
}
switch ($type)
{
case ':': //ARRAY (Multi Flexi) 1 to 10
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 '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 = $qinfo['sgqa'] . trim($exclusive_option) . '.NAOK';
}
else
{
$sq_name = $qinfo['sgqa'] . trim($exclusive_option) . '.NAOK';
}
break;
default:
break;
}
if (!is_null($sq_name)) {
$subQrels[] = array(
'qtype' => $type,
'type' => 'exclude_all_others',
'rowdivid' => $sq['rowdivid'],
'eqn' => 'is_empty(' . $sq_name . ')',
'qid' => $questionNum,
'sgqa' => $qinfo['sgqa'],
);
}
}
}
}
}
// exclude_all_others_auto
// if (count(this.relevanceStatus) == count(this)) { set exclusive option value to "Y" and call checkconditions() }
// However, note that would need to blank the values, not use relevance, otherwise can't unclick the _auto option without having it re-enable itself
if (isset($qattr['exclude_all_others_auto']) && trim($qattr['exclude_all_others_auto']) == '1'
&& isset($qattr['exclude_all_others']) && trim($qattr['exclude_all_others']) != '' && count(explode(';',trim($qattr['exclude_all_others']))) == 1)
{
$exclusive_option = trim($qattr['exclude_all_others']);
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'M': //Multiple choice checkbox
case 'P': //Multiple choice with comments checkbox + text
if ($this->sgqaNaming)
{
$sq_name = substr($sq['jsVarName'],4);
}
else
{
$sq_name = $sq['varName'];
}
break;
default:
break;
}
if (!is_null($sq_name))
{
if ($sq['csuffix'] == $exclusive_option)
{
$eoVarName = substr($sq['jsVarName'],4);
}
else
{
$sq_names[] = $sq_name;
}
}
}
if (count($sq_names) > 0 && isset($eoVarName)) { // eoVarName not set : exclude option don't exist in sub question code
$relpart = "sum(" . implode(".relevanceStatus, ", $sq_names) . ".relevanceStatus)";
$checkedpart = "count(" . implode(".NAOK, ", $sq_names) . ".NAOK)";
$eoRelevantAndUnchecked = "(" . $eoVarName . ".relevanceStatus && is_empty(" . $eoVarName . "))";
$eoEqn = "(" . $eoRelevantAndUnchecked . " && (" . $relpart . " == " . $checkedpart . "))";
$this->em->ProcessBooleanExpression($eoEqn, $qinfo['gseq'], $qinfo['qseq']);
$relevanceVars = implode('|',$this->em->GetJSVarsUsed());
$relevanceJS = $this->em->GetJavaScriptEquivalentOfExpression();
// Unset all checkboxes and hidden values for this question (irregardless of whether they are array filtered)
$eosaJS = "if (" . $relevanceJS . ") {\n";
$eosaJS .=" $('#question" . $questionNum . " [type=checkbox]').prop('checked',false);\n";
$eosaJS .=" $('#java" . $qinfo['sgqa'] . "other').val('');\n";
$eosaJS .=" $('#answer" . $qinfo['sgqa'] . "other').val('');\n";
$eosaJS .=" $('[id^=java" . $qinfo['sgqa'] . "]').val('');\n";
$eosaJS .=" $('#answer" . $eoVarName . "').prop('checked',true);\n";
$eosaJS .=" $('#java" . $eoVarName . "').val('Y');\n";
$eosaJS .=" LEMrel" . $questionNum . "();\n";
$eosaJS .=" relChange" . $questionNum ."=true;\n";
$eosaJS .="}\n";
$this->qid2exclusiveAuto[$questionNum] = array(
'js'=>$eosaJS,
'relevanceVars'=>$relevanceVars, // so that EM knows which variables to declare
'rowdivid'=>$eoVarName, // to ensure that EM creates a hidden relevanceSGQA input for the exclusive option
);
}
}
}
// input_boxes
if (isset($qattr['input_boxes']) && $qattr['input_boxes'] == 1) {
$input_boxes=1;
switch($type)
{
case ':': //Array Numbers
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_equs=array();
$subqValidEqns = array();
foreach($subqs as $sq)
{
$sq_name = ($this->sgqaNaming)?(string)substr($sq['jsVarName'],4).".NAOK":$sq['varName'].".NAOK";
$sq_equ = '( is_numeric('.$sq_name.') || is_empty('.$sq_name.') )';// Leave mandatory to mandatory attribute (see #08665)
$subqValidSelector = $sq['jsVarName_on'];
if (!is_null($sq_name)) {
$sq_equs[] = $sq_equ;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_equ,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'input_boxes',
'class' => 'input_boxes',
'eqn' => implode(' and ',$sq_equs),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
break;
default:
break;
}
}else{
$input_boxes="";
}
// min_answers
// Validation:= count(sq1,...,sqN) >= value (which could be an expression).
if (isset($qattr['min_answers']) && trim($qattr['min_answers']) != '' && trim($qattr['min_answers']) != '0')
{
$min_answers = $qattr['min_answers'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case '1': //Array (Flexible Labels) dual scale
if (substr($sq['varName'],-1,1) == '0')
{
if ($this->sgqaNaming)
{
$base = $sq['rowdivid']."#";
$sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
}
else
{
$base = (string)substr($sq['varName'],0,-1);
$sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
}
}
break;
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 'K': //MULTIPLE NUMERICAL QUESTION
case 'Q': //MULTIPLE SHORT TEXT
case 'M': //Multiple choice checkbox
case 'R': //RANKING STYLE
if ($this->sgqaNaming)
{
$sq_name = (string)substr($sq['jsVarName'],4) . '.NAOK';
}
else
{
$sq_name = $sq['varName'] . '.NAOK';
}
break;
case 'P': //Multiple choice with comments checkbox + text
if (!preg_match('/comment$/',$sq['varName'])) {
if ($this->sgqaNaming)
{
$sq_name = $sq['rowdivid'] . '.NAOK';
}
else
{
$sq_name = $sq['rowdivid'] . '.NAOK';
}
}
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'min_answers',
'class' => 'num_answers',
'eqn' => 'if(is_empty('.$min_answers.'),1,(count(' . implode(', ', $sq_names) . ') >= (' . $min_answers . ')))',
'qid' => $questionNum,
);
}
}
}
else
{
$min_answers='';
}
// max_answers
// Validation:= count(sq1,...,sqN) <= value (which could be an expression).
if (isset($qattr['max_answers']) && trim($qattr['max_answers']) != '')
{
$max_answers = $qattr['max_answers'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case '1': //Array (Flexible Labels) dual scale
if (substr($sq['varName'],-1,1) == '0')
{
if ($this->sgqaNaming)
{
$base = $sq['rowdivid']."#";
$sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
}
else
{
$base = substr($sq['varName'],0,-1);
$sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
}
}
break;
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 'K': //MULTIPLE NUMERICAL QUESTION
case 'Q': //MULTIPLE SHORT TEXT
case 'M': //Multiple choice checkbox
case 'R': //RANKING STYLE
if ($this->sgqaNaming)
{
$sq_name = substr($sq['jsVarName'],4) . '.NAOK';
}
else
{
$sq_name = $sq['varName'] . '.NAOK';
}
break;
case 'P': //Multiple choice with comments checkbox + text
if (!preg_match('/comment$/',$sq['varName'])) {
if ($this->sgqaNaming)
{
$sq_name = $sq['rowdivid'] . '.NAOK';
}
else
{
$sq_name = $sq['varName'] . '.NAOK';
}
}
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'max_answers',
'class' => 'num_answers',
'eqn' => '(if(is_empty('.$max_answers.'),1,count(' . implode(', ', $sq_names) . ') <= (' . $max_answers . ')))',
'qid' => $questionNum,
);
}
}
}
else
{
$max_answers='';
}
/* Specific for ranking : fix only the alert : test if needed (max_subquestions < count(answers) )*/
if($type=='R' && (isset($qattr['max_subquestions']) && intval($qattr['max_subquestions'])>0))
{
$max_subquestions=intval($qattr['max_subquestions']);
// We don't have another answer count in EM ?
$answerCount = Answer::model()->count("qid=:qid and language=:language",array(":qid"=>$questionNum,'language'=>$_SESSION['LEMlang']));
$max_subquestions = min($max_subquestions,$answerCount); // Can not be upper than current answers #14899
if($max_answers!='')
{
$max_answers='min('.$max_answers.','.$max_subquestions.')';
}
else
{
$max_answers= $max_subquestions;
}
}
// Fix min_num_value_n and max_num_value_n for multinumeric with slider: see bug #7798
if($type=="K" && isset($qattr['slider_min']) && ( !isset($qattr['min_num_value_n']) || trim($qattr['min_num_value_n'])==''))
$qattr['min_num_value_n']=$qattr['slider_min'];
// min_num_value_n
// Validation:= N >= value (which could be an expression).
if (isset($qattr['min_num_value_n']) && trim($qattr['min_num_value_n']) != '')
{
$min_num_value_n = $qattr['min_num_value_n'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'K': //MULTIPLE NUMERICAL QUESTION
if ($this->sgqaNaming)
{
$sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK >= (' . $min_num_value_n . '))';
}
else
{
$sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK >= (' . $min_num_value_n . '))';
}
$subqValidSelector = $sq['jsVarName_on'];
break;
case 'N': //NUMERICAL QUESTION TYPE
if ($this->sgqaNaming)
{
$sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK >= (' . $min_num_value_n . '))';
}
else
{
$sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK >= (' . $min_num_value_n . '))';
}
$subqValidSelector = '';
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_name,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'min_num_value_n',
'class' => 'value_range',
'eqn' => implode(' && ', $sq_names),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$min_num_value_n='';
}
// Fix min_num_value_n and max_num_value_n for multinumeric with slider: see bug #7798
if($type=="K" && isset($qattr['slider_max']) && ( !isset($qattr['max_num_value_n']) || trim($qattr['max_num_value_n'])==''))
$qattr['max_num_value_n']=$qattr['slider_max'];
// max_num_value_n
// Validation:= N <= value (which could be an expression).
if (isset($qattr['max_num_value_n']) && trim($qattr['max_num_value_n']) != '')
{
$max_num_value_n = $qattr['max_num_value_n'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'K': //MULTIPLE NUMERICAL QUESTION
if ($this->sgqaNaming)
{
$sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK <= (' . $max_num_value_n . '))';
}
else
{
$sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK <= (' . $max_num_value_n . '))';
}
$subqValidSelector = $sq['jsVarName_on'];
break;
case 'N': //NUMERICAL QUESTION TYPE
if ($this->sgqaNaming)
{
$sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK <= (' . $max_num_value_n . '))';
}
else
{
$sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK <= (' . $max_num_value_n . '))';
}
$subqValidSelector = '';
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_name,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'max_num_value_n',
'class' => 'value_range',
'eqn' => implode(' && ', $sq_names),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$max_num_value_n='';
}
// min_num_value
// Validation:= sum(sq1,...,sqN) >= value (which could be an expression).
if (isset($qattr['min_num_value']) && trim($qattr['min_num_value']) != '')
{
$min_num_value = $qattr['min_num_value'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'K': //MULTIPLE NUMERICAL QUESTION
if ($this->sgqaNaming)
{
$sq_name = $sq['rowdivid'] . '.NAOK';
}
else
{
$sq_name = $sq['varName'] . '.NAOK';
}
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$sumEqn = 'sum(' . implode(', ', $sq_names) . ')';
$precision = LEM_DEFAULT_PRECISION;
if (!is_null($precision))
{
$sumEqn = 'round(' . $sumEqn . ', ' . $precision . ')';
}
$noanswer_option = '';
if ($value_range_allows_missing)
{
$noanswer_option = ' || count(' . implode(', ', $sq_names) . ') == 0';
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'min_num_value',
'class' => 'sum_range',
'eqn' => '(sum(' . implode(', ', $sq_names) . ') >= (' . $min_num_value . ')' . $noanswer_option . ')',
'qid' => $questionNum,
'sumEqn' => $sumEqn,
);
}
}
}
else
{
$min_num_value='';
}
// max_num_value
// Validation:= sum(sq1,...,sqN) <= value (which could be an expression).
if (isset($qattr['max_num_value']) && trim($qattr['max_num_value']) != '')
{
$max_num_value = $qattr['max_num_value'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case 'K': //MULTIPLE NUMERICAL QUESTION
if ($this->sgqaNaming)
{
$sq_name = $sq['rowdivid'] . '.NAOK';
}
else
{
$sq_name = $sq['varName'] . '.NAOK';
}
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$sumEqn = 'sum(' . implode(', ', $sq_names) . ')';
$precision = LEM_DEFAULT_PRECISION;
if (!is_null($precision))
{
$sumEqn = 'round(' . $sumEqn . ', ' . $precision . ')';
}
$noanswer_option = '';
if ($value_range_allows_missing)
{
$noanswer_option = ' || count(' . implode(', ', $sq_names) . ') == 0';
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'max_num_value',
'class' => 'sum_range',
'eqn' => '(sum(' . implode(', ', $sq_names) . ') <= (' . $max_num_value . ')' . $noanswer_option . ')',
'qid' => $questionNum,
'sumEqn' => $sumEqn,
);
}
}
}
else
{
$max_num_value='';
}
// multiflexible_min
// Validation:= sqN >= value (which could be an expression).
if (isset($qattr['multiflexible_min']) && trim($qattr['multiflexible_min']) != '' && $input_boxes=='1')
{
$multiflexible_min = $qattr['multiflexible_min'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case ':': //MULTIPLE NUMERICAL QUESTION
if ($this->sgqaNaming)
{
$sgqa = (string)substr($sq['jsVarName'],4);
$sq_name = '(is_empty(' . $sgqa . '.NAOK) || ' . $sgqa . '.NAOK >= (' . $multiflexible_min . '))';
}
else
{
$sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || ' . $sq['varName'] . '.NAOK >= (' . $multiflexible_min . '))';
}
$subqValidSelector = $sq['jsVarName_on'];
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_name,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'multiflexible_min',
'class' => 'value_range',
'eqn' => implode(' && ', $sq_names),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$multiflexible_min='';
}
// multiflexible_max
// Validation:= sqN <= value (which could be an expression).
if (isset($qattr['multiflexible_max']) && trim($qattr['multiflexible_max']) != '' && $input_boxes=='1')
{
$multiflexible_max = $qattr['multiflexible_max'];
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_names = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_name = NULL;
switch ($type)
{
case ':': //MULTIPLE NUMERICAL QUESTION
if ($this->sgqaNaming)
{
$sgqa = substr($sq['jsVarName'],4);
$sq_name = '(is_empty(' . $sgqa . '.NAOK) || ' . $sgqa . '.NAOK <= (' . $multiflexible_max . '))';
}
else
{
$sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || ' . $sq['varName'] . '.NAOK <= (' . $multiflexible_max . '))';
}
$subqValidSelector = $sq['jsVarName_on'];
break;
default:
break;
}
if (!is_null($sq_name)) {
$sq_names[] = $sq_name;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_name,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (count($sq_names) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'multiflexible_max',
'class' => 'value_range',
'eqn' => implode(' && ', $sq_names),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$multiflexible_max='';
}
// min_num_of_files
// Validation:= sq_filecount >= value (which could be an expression).
if (isset($qattr['min_num_of_files']) && trim($qattr['min_num_of_files']) != '' && trim($qattr['min_num_of_files']) != '0')
{
$min_num_of_files = $qattr['min_num_of_files'];
$eqn='';
$sgqa = $qinfo['sgqa'];
switch ($type)
{
case '|': //List - dropdown
$eqn = "(" . $sgqa . "_filecount >= (" . $min_num_of_files . "))";
break;
default:
break;
}
if ($eqn != '')
{
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'min_num_of_files',
'class' => 'num_answers',
'eqn' => $eqn,
'qid' => $questionNum,
);
}
}
else
{
$min_num_of_files = '';
}
// max_num_of_files
// Validation:= sq_filecount <= value (which could be an expression).
if (isset($qattr['max_num_of_files']) && trim($qattr['max_num_of_files']) != '')
{
$max_num_of_files = $qattr['max_num_of_files'];
$eqn='';
$sgqa = $qinfo['sgqa'];
switch ($type)
{
case '|': //List - dropdown
$eqn = "(" . $sgqa . "_filecount <= (" . $max_num_of_files . "))";
break;
default:
break;
}
if ($eqn != '')
{
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'max_num_of_files',
'class' => 'num_answers',
'eqn' => $eqn,
'qid' => $questionNum,
);
}
}
else
{
$max_num_of_files = '';
}
// num_value_int_only
// Validation fixnum(sqN)==int(fixnum(sqN)) : fixnum or not fix num ..... 10.00 == 10
if (isset($qattr['num_value_int_only']) && trim($qattr['num_value_int_only']) == "1")
{
$num_value_int_only="1";
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_eqns = array();
$subqValidEqns = array();
foreach ($subqs as $sq) {
$sq_eqn=null;
$subqValidSelector = '';
switch ($type)
{
case 'K': //MULTI NUMERICAL QUESTION TYPE (Need a attribute, not set in 131014)
$subqValidSelector = $sq['jsVarName_on'];
case 'N': //NUMERICAL QUESTION TYPE
$sq_name = ($this->sgqaNaming)?$sq['rowdivid'].".NAOK":$sq['varName'].".NAOK";
$sq_eqn = '( is_int('.$sq_name.') || is_empty('.$sq_name.') )';
break;
default:
break;
}
if (!is_null($sq_eqn)) {
$sq_eqns[] = $sq_eqn;
$subqValidEqns[$subqValidSelector] = array(
'subqValidEqn' => $sq_eqn,
'subqValidSelector' => $subqValidSelector,
);
}
}
if (count($sq_eqns) > 0) {
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'num_value_int_only',
'class' => 'value_integer',
'eqn' => implode(' and ', $sq_eqns),
'qid' => $questionNum,
'subqValidEqns' => $subqValidEqns,
);
}
}
}
else
{
$num_value_int_only='';
}
// num_value_int_only
// Validation is_numeric(sqN)
if (isset($qattr['numbers_only']) && trim($qattr['numbers_only']) == "1")
{
$numbers_only=1;
switch ($type)
{
case 'S': // Short text
if ($hasSubqs) {
$subqs = $qinfo['subqs'];
$sq_equs=array();
foreach($subqs as $sq)
{
$sq_name = ($this->sgqaNaming)?$sq['rowdivid'].".NAOK":$sq['varName'].".NAOK";
$sq_equs[] = '( is_numeric('.$sq_name.') || is_empty('.$sq_name.') )';
}
if (!isset($validationEqn[$questionNum]))
{
$validationEqn[$questionNum] = array();
}
$validationEqn[$questionNum][] = array(
'qtype' => $type,
'type' => 'numbers_only',
'class' => 'numbers_only',
'eqn' => implode(' and ',$sq_equs),
'qid' => $questionNum,
);
}