PageRenderTime 110ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 2ms

/application/helpers/expressions/em_manager_helper.php

https://bitbucket.org/machaven/limesurvey
PHP | 9004 lines | 7249 code | 495 blank | 1260 comment | 1548 complexity | 55a51b53a63b01a392dbf89612886366 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, BSD-3-Clause, GPL-3.0, LGPL-3.0
  1. <?php
  2. /**
  3. * LimeSurvey
  4. * Copyright (C) 2007-2011 The LimeSurvey Project Team / Carsten Schmitz
  5. * All rights reserved.
  6. * License: GNU/GPL License v2 or later, see LICENSE.php
  7. * LimeSurvey is free software. This version may have been modified pursuant
  8. * to the GNU General Public License, and as distributed it includes or
  9. * is derivative of works licensed under the GNU General Public License or
  10. * other free or open source software licenses.
  11. * See COPYRIGHT.php for copyright notices and details.
  12. *
  13. * $Id$
  14. */
  15. /**
  16. * Description of LimeExpressionManager
  17. * This is a wrapper class around ExpressionManager that implements a Singleton and eases
  18. * passing of LimeSurvey variable values into ExpressionManager
  19. *
  20. * @author Thomas M. White (TMSWhite)
  21. */
  22. include_once('em_core_helper.php');
  23. Yii::app()->loadHelper('database');
  24. Yii::app()->loadHelper('frontend');
  25. Yii::import("application.libraries.Date_Time_Converter");
  26. define('LEM_DEBUG_TIMING',1);
  27. define('LEM_DEBUG_VALIDATION_SUMMARY',2); // also includes SQL error messages
  28. define('LEM_DEBUG_VALIDATION_DETAIL',4);
  29. define('LEM_PRETTY_PRINT_ALL_SYNTAX',32);
  30. define('LEM_DEFAULT_PRECISION',12);
  31. class LimeExpressionManager {
  32. /**
  33. * LimeExpressionManager is a singleton. $instance is its storage location.
  34. * @var LimeExpressionManager
  35. */
  36. private static $instance;
  37. /**
  38. * Implements the recursive descent parser that processes expressions
  39. * @var ExpressionManager
  40. */
  41. private $em;
  42. /**
  43. *
  44. * @var type
  45. */
  46. private $groupRelevanceInfo;
  47. /**
  48. * The survey ID
  49. * @var integer
  50. */
  51. private $sid;
  52. /**
  53. * sum of LEM_DEBUG constants - use bitwise AND comparisons to identify which parts to use
  54. * @var type
  55. */
  56. private $debugLevel=0;
  57. /**
  58. * Collection of variable attributes, indexed by SGQA code
  59. *
  60. * Actual variables are stored in this structure:
  61. * $knownVars[$sgqa] = array(
  62. * 'jsName_on' => // the name of the javascript variable if it is defined on the current page - often 'answerSGQA'
  63. * 'jsName' => // the name of the javascript variable when referenced on different pages - usually 'javaSGQA'
  64. * 'readWrite' => // 'Y' for yes, 'N' for no - currently not used
  65. * 'hidden' => // 1 if the question attribute 'hidden' is true, otherwise 0
  66. * 'question' => // the text of the question (or sub-question)
  67. * 'qid' => // the numeric question id - e.g. the Q part of the SGQA name
  68. * 'gid' => // the numeric group id - e.g. the G part of the SGQA name
  69. * 'grelevance' => // the group level relevance string
  70. * 'relevance' => // the question level relevance string
  71. * 'qcode' => // the qcode-style variable name for this question (or sub-question)
  72. * 'qseq' => // the 0-based index of the question within the survey
  73. * 'gseq' => // the 0-based index of the group within the survey
  74. * 'type' => // the single character type code for the question
  75. * 'sgqa' => // the SGQA name for the variable
  76. * 'ansList' => // ansArray converted to a JavaScript fragment - e.g. ",'answers':{ 'M':'Male','F':'Female'}"
  77. * 'ansArray' => // PHP array of answer strings, keyed on the answer code = e.g. array['M']='Male';
  78. * 'scale_id' => // '0' for most answers. '1' for second scale within dual-scale questions
  79. * 'rootVarName' => // the root code / name / title for the question, without any sub-question or answer-level suffix. This is from the title column in the questions table
  80. * 'subqtext' => // the sub-question text
  81. * 'rowdivid' => // the JavaScript ID of the row identifier for a question. This is used to show/hide entire question rows
  82. * '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
  83. * );
  84. *
  85. * Reserved variables (e.g. TOKEN:xxxx) are stored with this structure:
  86. * $knownVars[$token] = array(
  87. * 'code' => // the static value for the variable
  88. * 'type' => // ''
  89. * 'jsName_on' => // ''
  90. * 'jsName' => // ''
  91. * 'readWrite' => // 'N' - since these are always read-only variables
  92. * );
  93. *
  94. * @var type
  95. */
  96. private $knownVars;
  97. /**
  98. * maps qcode varname to SGQA code
  99. *
  100. * @example ['gender'] = '38612X10X145'
  101. * @var type
  102. */
  103. private $qcode2sgqa;
  104. /**
  105. * variables temporarily set for substitution purposes
  106. *
  107. * These are typically the LimeReplacement Fields passed in via templatereplace()
  108. * Each has the following structure: array(
  109. * 'code' => // the static value of the variable
  110. * 'jsName_on' => // ''
  111. * 'jsName' => // ''
  112. * 'readWrite' => // 'N'
  113. * );
  114. *
  115. * @var type
  116. */
  117. private $tempVars;
  118. /**
  119. * Array of relevance information for each page (gseq), indexed by gseq.
  120. * Within a page, it contains a sequential list of the results of each relevance equation processed
  121. * array(
  122. * 'qid' => // question id -- e.g. 154
  123. * 'gseq' => // 0-based group sequence -- e.g. 2
  124. * 'eqn' => // the raw relevance equation parsed -- e.g. "!is_empty(p2_sex)"
  125. * 'result' => // the Boolean result of parsing that equation in the current context -- e.g. 0
  126. * 'numJsVars' => // the number of dynamic JavaScript variables used in that equation -- e.g. 1
  127. * 'relevancejs' => // the actual JavaScript to insert for that relevance equation -- e.g. "LEMif(LEManyNA('p2_sex'),'',( ! LEMempty(LEMval('p2_sex') )))"
  128. * 'relevanceVars' => // a pipe-delimited list of JavaScript variables upon which that equation depends -- e.g. "java38612X12X153"
  129. * 'jsResultVar' => // the JavaScript variable in which that result will be stored -- e.g. "java38612X12X154"
  130. * 'type' => // the single character type of the question -- e.g. 'S'
  131. * 'hidden' => // 1 if the question should always be hidden
  132. * 'hasErrors' => // 1 if there were parsing errors processing that relevance equation
  133. * @var type
  134. */
  135. private $pageRelevanceInfo;
  136. /**
  137. *
  138. * @var type
  139. */
  140. private $pageTailorInfo;
  141. /**
  142. * internally set to true (1) for survey.php so get group-specific logging but keep javascript variable namings consistent on the page.
  143. * @var type
  144. */
  145. private $allOnOnePage=false;
  146. /**
  147. * survey mode. One of 'survey', 'group', or 'question'
  148. * @var string
  149. */
  150. private $surveyMode='group';
  151. /**
  152. * a set of global survey options passed from LimeSurvey
  153. *
  154. * For example, array(
  155. * 'rooturl' => // URL prefix needed to be able to click on a syntax-highlighted variable name and have it open the needed editting window
  156. * 'hyperlinkSyntaxHighlighting' => // true if should be able to click on variables to edit them
  157. * 'active' => // 0 for inactive, 1 for active survey
  158. * 'allowsave' => // 0 for do not allow save; 1 for allow save
  159. * 'anonymized' => // 1 for anonymous
  160. * 'assessments' => // 1 for use assessments
  161. * 'datestamp' => // 1 for use date stamps
  162. * 'ipaddr' => // 1 for capture IP address
  163. * 'radix' => // '.' for use period as decimal separator; ',' for use comma as decimal separator
  164. * 'savetimings' => // "Y" if should save survey timings
  165. * 'startlanguage' => // the starting language -- e.g. 'en'
  166. * 'surveyls_dateformat' => // the index of the language specific date format -- e.g. 1
  167. * 'tablename' => // the name of the table storing the survey data, if active -- e.g. lime_survey_38612
  168. * 'target' => // the path for uploading files -- e.g. '/temp/files/'
  169. * 'timeadjust' => // the time offset -- e.g. 0
  170. * 'tempdir' => // the temporary directory for uploading files -- e.g. '/temp/'
  171. * );
  172. *
  173. * @var type
  174. */
  175. private $surveyOptions=array();
  176. /**
  177. * array of mappings of Question # (qid) to pipe-delimited list of SGQA codes used within it
  178. *
  179. * @example [150] = "38612X11X150|38612X11X150other"
  180. * @var type
  181. */
  182. private $qid2code;
  183. /**
  184. * array of mappings of JavaScript Variable names to Question number (qid)
  185. *
  186. * @example ['java38612X13X161other'] = '161'
  187. * @var type
  188. */
  189. private $jsVar2qid;
  190. /**
  191. * maps name of the variable to the SGQ name (without the A suffix)
  192. *
  193. * @example ['p1_sex'] = "38612X10X147"
  194. * @example ['afDS_sq1_1'] = "26626X37X705sq1#1"
  195. * @var type
  196. */
  197. private $qcode2sgq;
  198. /**
  199. * array of mappings of knownVar aliases to the JavaScript variable names.
  200. * This maps both the SGQA and qcode alias names to the same 2 dimensional array
  201. *
  202. * @example ['p1_sex'] = array(
  203. * 'jsName' => // the JavaScript variable name used by EM -- e.g. "java38612X11X147"
  204. * 'jsPart' => // the JavaScript fragment used in EM's ____ array -- e.g. "'p1_sex':'java38612X11X147'"
  205. * );
  206. * @example ['afDS_sq1_1] = array(
  207. * 'jsName' => "java26626X37X705sq1#1"
  208. * 'jsPart' => "'afDS_sq1_1':'java26626X37X705sq1#1'"
  209. * );
  210. * @var type
  211. */
  212. private $alias2varName;
  213. /**
  214. * JavaScript array of mappings of canonical JavaScript variable name to key attributes.
  215. * These fragments are used to create the JavaScript varNameAttr array.
  216. *
  217. * @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'}}"
  218. * @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'}}"
  219. *
  220. * @var type
  221. */
  222. private $varNameAttr;
  223. /**
  224. * array of enumerated answer lists indexed by qid
  225. * These use a tilde syntax to indicate which scale the answer is part of.
  226. *
  227. * @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'
  228. * @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'
  229. * @example // TODO - add example from survey using assessments
  230. *
  231. * @var type
  232. */
  233. private $qans;
  234. /**
  235. * map of gid to 0-based sequence number of groups
  236. *
  237. * @example [10] = 0 // means that the first group (gseq=0) has gid=10
  238. *
  239. * @var type
  240. */
  241. private $groupId2groupSeq;
  242. /**
  243. * map question # to an incremental count of question order across the whole survey
  244. *
  245. * @example [157] = 13 // means that that 14th question in the survey has qid=157
  246. *
  247. * @var type
  248. */
  249. private $questionId2questionSeq;
  250. /**
  251. * map question # to the group it is within, using an incremental count of group order
  252. *
  253. * @example [157] = 2 // means that qid 157 is in the 3rd page of questions (gseq = 2)
  254. *
  255. * @var type
  256. */
  257. private $questionId2groupSeq;
  258. /**
  259. * array of info about each Group, indexed by GroupSeq
  260. *
  261. * @example [2] = array(
  262. * 'qstart' => 9 // the first qseq within that group
  263. * 'qend' => 13 //the last qseq within that group
  264. * );
  265. *
  266. * @var type
  267. */
  268. private $groupSeqInfo;
  269. /**
  270. * tracks which groups have at least one relevant, non-hidden question
  271. *
  272. * @example [2] = 0 // means that the third group (gseq==2) is currently irrelevant
  273. *
  274. * @var type
  275. */
  276. private $gseq2relevanceStatus;
  277. /**
  278. * maps question # to the validation equation(s) for that question.
  279. * These are grouped by qid then validation type, such as 'value_range', and 'num_answers'
  280. *
  281. * @example [703] = array(
  282. * 'eqn' => array(
  283. * 'value_range' = "((is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK >= (0)) and (is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK <= (5)))"
  284. * ),
  285. * 'tips' => array(
  286. * 'value_range' = "Each answer must be between {fixnum(0)} and {fixnum(5)}"
  287. * ),
  288. * 'subqValidEqns' = array(
  289. * [] = array(
  290. * 'subqValidSelector' => '' //
  291. * 'subqValidEqn' => "(is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK >= (0)) && (is_empty(26626X34X703.NAOK) || 26626X34X703.NAOK <= (5))"
  292. * ),
  293. * 'sumEqn' => '' // the equation to compute the current sum of the responses
  294. * '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)
  295. * );
  296. *
  297. * @var type
  298. */
  299. private $qid2validationEqn;
  300. /**
  301. * keeps relevance in proper sequence so can minimize relevance processing to see what should be see on page and in indexes
  302. * Array is indexed on qseq
  303. *
  304. * @example [3] = array(
  305. * 'relevance' => "!is_empty(num)" // the question-level relevance equation
  306. * 'grelevance' => "" // the group-level relevance equation
  307. * 'qid' => "699" // the question id
  308. * 'qseq' => 3 // the 0-index question sequence
  309. * 'gseq' => 0 // the 0-index group sequence
  310. * 'jsResultVar_on' => 'answer26626X34X699' // the javascript variable holding the input value
  311. * 'jsResultVar' => 'java26226X34X699' // the javascript variable (often hidden) holding the value to be submitted
  312. * 'type' => 'N' // the one character question type
  313. * 'hidden' => 0 // 1 if it should be always_hidden
  314. * 'gid' => "34" // group id
  315. * 'mandatory' => 'N' // 'Y' if mandatory
  316. * 'eqn' => "" // TODO ??
  317. * 'help' => "" // the help text
  318. * 'qtext' => "Enter a larger number than {num}" // the question text
  319. * 'code' => 'afDS_sq5_1' // the full variable name
  320. * 'other' => 'N' // whether the question supports the 'other' option - 'Y' if true
  321. * 'rowdivid' => '2626X37X705sq5' // the javascript id for the row - in this case, the 5th sub-question
  322. * 'aid' => 'sq5' // the answer id
  323. * 'sqid' => '791' // the sub-question's qid (only populated for some question types)
  324. * );
  325. *
  326. * @var type
  327. */
  328. private $questionSeq2relevance;
  329. /**
  330. * current Group sequence (0-based index)
  331. * @example 1
  332. * @var integer
  333. */
  334. private $currentGroupSeq;
  335. /**
  336. * for Question-by-Question mode, the 0-based index
  337. * @example 3
  338. * @var integer
  339. */
  340. private $currentQuestionSeq;
  341. /**
  342. * used in Question-by-Question mode
  343. * @var integer
  344. */
  345. private $currentQID;
  346. /**
  347. * set of the current set of questions to be displayed, indexed by QID - at least one must be relevant
  348. *
  349. * The array has N entries, where N is the number if qids in the Qset. Each has the following contents:
  350. * @example [705] = array(
  351. * 'info' => array() // this is an exact copy of $questionSeq2relevance[$qseq] -- TODO - remove redundancy
  352. * 'relevant' => 1 // 1 if the question is currently relevant
  353. * 'hidden' => 0 // 1 if the question is always hidden
  354. * 'relEqn' => '' // the relevance equation -- TODO - how different from ['info']['relevance']?
  355. * '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"
  356. * '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"
  357. * 'valid' => 0 // 1 if the current answers pass all of the validation criteria for the question
  358. * '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)))"
  359. * 'prettyValidEqn' => // syntax-highlighted version of validEqn, only showing syntax errors
  360. * '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>"
  361. * '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>"
  362. * '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') )))))"
  363. * '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"
  364. * '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"
  365. * 'irrelevantSQs' => // current list of subquestions that are irrelevant -- e.g. "26626X37X705sq2#0|26626X37X705sq2#1|26626X37X705sq4#0|26626X37X705sq4#1"
  366. * 'subQrelEqn' => // TODO - ??
  367. * 'mandViolation' => 0 // 1 if the question is mandatory and fails the mandatory criteria
  368. * 'anyUnanswered' => 1 // 1 if any parts of the question are unanswered
  369. * 'mandTip' => '' // message to display if the question fails mandatory criteria
  370. * 'message' => '' // TODO ??
  371. * 'updatedValues' => // array of values that should be updated for this question, as [$sgqa] = $value
  372. * 'sumEqn' => '' //
  373. * 'sumRemainingEqn' => '' //
  374. * );
  375. *
  376. * @var type
  377. */
  378. private $currentQset=NULL;
  379. /**
  380. * last result of NavigateForwards, NavigateBackwards, or JumpTo
  381. * Array of status information about last movement, whether at question, group, or survey level
  382. *
  383. * @example = array(
  384. * 'finished' => 0 // 1 if the survey has been completed and needs to be finalized
  385. * 'message' => '' // any error message that needs to be displayed
  386. * 'seq' => 1 // the sequence count, using gseq, or qseq units if in 'group' or 'question' mode, respectively
  387. * 'mandViolation' => 0 // whether there was any violation of mandatory constraints in the last movement
  388. * 'valid' => 0 // 1 if the last movement passed all validation constraints. 0 if there were any validation errors
  389. * 'unansweredSQs' => // pipe-separated list of any sub-questions that were not answered
  390. * 'invalidSQs' => // pipe-separated list of any sub-questions that failed validation constraints
  391. * );
  392. *
  393. * @var type
  394. */
  395. private $lastMoveResult=NULL;
  396. /**
  397. * array of information needed to generate navigation index in question-by-question mode
  398. * One entry for each question, indexed by qseq
  399. *
  400. * @example [4] = array(
  401. * 'qid' => "700" // the question id
  402. * 'qtext' => 'How old are you?' // the question text
  403. * 'qcode' => 'age' // the variable name
  404. * 'qhelp' => '' // the help text
  405. * 'anyUnanswered' => 0 // 1 if there are any sub-questions answered. Used for index display
  406. * 'anyErrors' => 0 // 1 if there are any errors among the sub-questions. Could be used for index display
  407. * 'show' => 1 // 1 if there are any relevant, non-hidden sub-questions. Only if so, then display the index entry
  408. * 'gseq' => 0 // the group sequence
  409. * 'gtext' => // text description for the group
  410. * 'gname' => 'G1' // the group title
  411. * 'gid' => "34" // the group id
  412. * 'mandViolation' => 0 // 1 if the question as a whole fails the mandatory criteria
  413. * 'valid' => 1 // 0 if any part of the question fails validation criteria.
  414. * );
  415. *
  416. * @var type
  417. */
  418. private $indexQseq;
  419. /**
  420. * array of information needed to generate navigation index in group-by-group mode
  421. * One entry for each group, indexed by gseq
  422. *
  423. * @example [0] = array(
  424. * 'gtext' => // the description for the group
  425. * 'gname' => 'G1' // the group title
  426. * 'gid' => '34' // the group id
  427. * 'anyUnanswered' => 0 // 1 if any questions within the group are unanswered
  428. * 'anyErrors' => 0 // 1 if any of the questions within the group fail either validity or mandatory constraints
  429. * 'valid' => 1 // 1 if at least question in the group is relevant and non-hidden
  430. * 'mandViolation' => 0 // 1 if at least one relevant, non-hidden question in the group fails mandatory constraints
  431. * 'show' => 1 // 1 if there is at least one relevant, non-hidden question within the group
  432. * );
  433. *
  434. * @var type
  435. */
  436. private $indexGseq;
  437. /**
  438. * array of group sequence number to static info
  439. * One entry per group, indexed on gseq
  440. *
  441. * @example [0] = array(
  442. * 'group_order' => 0 // gseq
  443. * 'gid' => "34" // group id
  444. * 'group_name' => 'G2' // the group title
  445. * 'description' => // the description of the group (e.g. gtitle)
  446. * 'grelevance' => '' // the group-level relevance
  447. * );
  448. *
  449. * @var type
  450. */
  451. private $gseq2info;
  452. /**
  453. * the maximum groupSeq reached - this is needed for Index
  454. * @var type
  455. */
  456. private $maxGroupSeq;
  457. /**
  458. * mapping of questions to information about their subquestions.
  459. * One entry per question, indexed on qid
  460. *
  461. * @example [702] = array(
  462. * 'qid' => 702 // the question id
  463. * 'qseq' => 6 // the question sequence
  464. * 'gseq' => 0 // the group sequence
  465. * 'sgqa' => '26626X34X702' // the root of the SGQA code (reallly just the SGQ)
  466. * 'varName' => 'afSrcFilter_sq1' // the full qcode variable name - note, if there are sub-questions, don't use this one.
  467. * 'type' => 'M' // the one-letter question type
  468. * 'fieldname' => '26626X34X702sq1' // the fieldname (used as JavaScript variable name, and also as database column name
  469. * 'rootVarName' => 'afDS' // the root variable name
  470. * 'preg' => '/[A-Z]+/' // regular expression validation equation, if any
  471. * 'subqs' => array() of sub-questions, where each contains:
  472. * 'rowdivid' => '26626X34X702sq1' // the javascript id identifying the question row (so array_filter can hide rows)
  473. * 'varName' => 'afSrcFilter_sq1' // the full variable name for the sub-question
  474. * 'jsVarName_on' => 'java26626X34X702sq1' // the JavaScript variable name if the variable is defined on the current page
  475. * 'jsVarName' => 'java26626X34X702sq1' // the JavaScript variable name to use if the variable is defined on a different page
  476. * 'csuffix' => 'sq1' // the SGQ suffix to use for a fieldname
  477. * 'sqsuffix' => '_sq1' // the suffix to use for a qcode variable name
  478. * );
  479. *
  480. * @var type
  481. */
  482. private $q2subqInfo;
  483. /**
  484. * array of advanced question attributes for each question
  485. * Indexed by qid; available for all quetions
  486. *
  487. * @example [784] = array(
  488. * 'array_filter_exclude' => 'afSrcFilter'
  489. * 'exclude_all_others' => 'sq5'
  490. * 'max_answers' => '3'
  491. * 'min_answers' => '1'
  492. * 'other_replace_text' => '{afSrcFilter_other}'
  493. * );
  494. *
  495. * @var type
  496. */
  497. private $qattr;
  498. /**
  499. * list of needed sub-question relevance (e.g. array_filter)
  500. * Indexed by qid then sgqa; only generated for current group of questions
  501. *
  502. * @example [708][26626X37X708sq2] = array(
  503. * 'qid' => '708' // the question id
  504. * 'eqn' => "((26626X34X702sq2 != ''))" // the auto-generated sub-question-level relevance equation
  505. * 'prettyPrintEqn' => '' // only generated if there errors - shows syntax highlighting of them
  506. * 'result' => 0 // result of processing the sub-question-level relevance equation in the current context
  507. * 'numJsVars' => 1 // the number of on-page javascript variables in 'eqn'
  508. * 'relevancejs' => // the generated javascript from 'eqn' -- e.g. "LEMif(LEManyNA('26626X34X702sq2'),'',(((LEMval('26626X34X702sq2') != ""))))"
  509. * 'relevanceVars' => "java26626X34X702sq2" // the pipe-separated list of on-page javascript variables in 'eqn'
  510. * 'rowdivid' => "26626X37X708sq2" // the javascript id of the question row (so can apply array_filter)
  511. * 'type' => 'array_filter' // semicolon delimited list of types of subquestion relevance filters applied
  512. * 'qtype' => 'A' // the single character question type
  513. * 'sgqa' => "26626X37X708" // the SGQ portion of the fieldname
  514. * 'hasErrors' => 0 // 1 if there are any parse errors in the sub-question validation equations
  515. * );
  516. *
  517. * @var type
  518. */
  519. private $subQrelInfo=array();
  520. /**
  521. * array of Group-level relevance status
  522. * Indexed by gseq; only shows groups that have been visited
  523. *
  524. * @example [1] = array(
  525. * 'gseq' => 1 // group sequence
  526. * 'eqn' => '' // the group-level relevance
  527. * 'result' => 1 // result of processing the group-level relevance
  528. * 'numJsVars' => 0 // the number of on-page javascript variables in the group-level relevance equation
  529. * 'relevancejs' => '' // the javascript version of the relevance equation
  530. * 'relevanceVars' => '' // the pipe-delimited list of on-page javascript variable names used within the group-level relevance equation
  531. * 'prettyPrint' => '' // a pretty-print version of the group-level relevance equation, only if there are errors
  532. * );
  533. *
  534. * @var type
  535. */
  536. private $gRelInfo=array();
  537. /**
  538. * Array of timing information to debug how long it takes for portions of LEM to run.
  539. * Array of timing information (in seconds) for EM to help with debugging
  540. *
  541. * @example [1] = array(
  542. * [0]="LimeExpressionManager::NavigateForwards"
  543. * [1]=1.7079849243164
  544. * );
  545. *
  546. * @var type
  547. */
  548. private $runtimeTimings=array();
  549. /**
  550. * True (1) if calling LimeExpressionManager functions between StartSurvey and FinishProcessingPage
  551. * Used (mostly deprecated) to detect calls to LEM which happen outside of the normal processing scope
  552. * @var Boolean
  553. */
  554. private $initialized=false;
  555. /**
  556. * True (1) if have already processed the relevance equations (so don't need to do it again)
  557. *
  558. * @var Boolean
  559. */
  560. private $processedRelevance=false;
  561. /**
  562. * Message generated to show debug timing values, if debugLevel includes LEM_DEBUG_TIMING
  563. * @var type
  564. */
  565. private $debugTimingMsg='';
  566. /**
  567. * temporary variable to reduce need to parse same equation multiple times. Used for relevance and validation
  568. * Array, indexed on equation, providing the following information:
  569. *
  570. * @example ['!is_empty(num)'] = array(
  571. * 'result' => 1 // result of processing the equation in the current scope
  572. * 'prettyPrint' => '' // syntax-highlighted version of equation if there are any errors
  573. * 'hasErrors' => 0 // 1 if there are any syntax errors
  574. * );
  575. *
  576. * @var type
  577. */
  578. private $ParseResultCache;
  579. /**
  580. * array of 2nd scale answer lists for types ':' and ';' -- needed for convenient print of logic file
  581. * Indexed on qid; available for all questions
  582. *
  583. * @example [706] = array(
  584. * '1~1' => '1|Never',
  585. * '1~2' => '2|Sometimes',
  586. * '1~3' => '3|Always'
  587. * );
  588. *
  589. * @var type
  590. */
  591. private $multiflexiAnswers;
  592. /**
  593. * used to specify whether to generate equations using SGQA codes or qcodes
  594. * Default is to convert all qcode naming to sgqa naming when generating javascript, as that provides the greatest backwards compatibility
  595. * TSV export of survey structure sets this to false so as to force use of qcode naming
  596. *
  597. * @var Boolean
  598. */
  599. private $sgqaNaming = true;
  600. /**
  601. * Number of groups in survey (number of possible pages to display)
  602. * @var integer
  603. */
  604. private $numGroups=0;
  605. /**
  606. * Numer of questions in survey (counting display-only ones?)
  607. * @var integer
  608. */
  609. private $numQuestions=0;
  610. /**
  611. * String identifier for the active session
  612. * @var type
  613. */
  614. private $sessid;
  615. /**
  616. * Linked list of array filters
  617. * @var array
  618. */
  619. private $qrootVarName2arrayFilter = array();
  620. /**
  621. * Array, keyed on qid, to JavaScript and list of variables needed to implement exclude_all_others_auto
  622. * @var type
  623. */
  624. private $qid2exclusiveAuto = array();
  625. /**
  626. * Array of values to be updated
  627. * @var type
  628. */
  629. private $updatedValues = array();
  630. /**
  631. * A private constructor; prevents direct creation of object
  632. */
  633. private function __construct()
  634. {
  635. self::$instance =& $this;
  636. $this->em = new ExpressionManager();
  637. if (!isset($_SESSION['LEMlang'])) {
  638. $_SESSION['LEMlang'] = 'en'; // so that there is a default
  639. }
  640. }
  641. /**
  642. * Ensures there is only one instances of LEM. Note, if switch between surveys, have to clear this cache
  643. * @return LimeExpressionManager
  644. */
  645. public static function &singleton()
  646. {
  647. $now = microtime(true);
  648. if (isset($_SESSION['LEMdirtyFlag'])) {
  649. $c = __CLASS__;
  650. self::$instance = new $c;
  651. unset($_SESSION['LEMdirtyFlag']);
  652. }
  653. else if (!isset(self::$instance)) {
  654. if (isset($_SESSION['LEMsingleton'])) {
  655. self::$instance = unserialize($_SESSION['LEMsingleton']);
  656. }
  657. else {
  658. $c = __CLASS__;
  659. self::$instance = new $c;
  660. }
  661. }
  662. else {
  663. // does exist, and OK to cache
  664. return self::$instance;
  665. }
  666. // only record duration if have to create new (or unserialize) an instance
  667. self::$instance->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  668. return self::$instance;
  669. }
  670. /**
  671. * Prevent users to clone the instance
  672. */
  673. public function __clone()
  674. {
  675. trigger_error('Clone is not allowed.', E_USER_ERROR);
  676. }
  677. /**
  678. * Tells Expression Manager that something has changed enough that needs to eliminate internal caching
  679. */
  680. public static function SetDirtyFlag()
  681. {
  682. $_SESSION['LEMdirtyFlag'] = true;
  683. $_SESSION['LEMforceRefresh'] = true;
  684. }
  685. /**
  686. * Set the SurveyId - really checks whether the survey you're about to work with is new, and if so, clears the LEM cache
  687. * @param <integer> $sid
  688. */
  689. public static function SetSurveyId($sid=NULL)
  690. {
  691. if (!is_null($sid)) {
  692. if (isset($_SESSION['LEMsid']) && $sid != $_SESSION['LEMsid']) {
  693. // then trying to use a new survey - so clear the LEM cache
  694. self::SetDirtyFlag();
  695. }
  696. $_SESSION['LEMsid'] = $sid;
  697. }
  698. }
  699. /**
  700. * Sets the language for Expression Manager. If the language has changed, then EM cache must be invalidated and refreshed
  701. * @param <string> $lang
  702. */
  703. public static function SetEMLanguage($lang=NULL)
  704. {
  705. if (is_null($lang)) {
  706. return; // should never happen
  707. }
  708. if (!isset($_SESSION['LEMlang'])) {
  709. $_SESSION['LEMlang'] = $lang;
  710. }
  711. if ($_SESSION['LEMlang'] != $lang) {
  712. // then changing languages, so clear cache
  713. self::SetDirtyFlag();
  714. }
  715. $_SESSION['LEMlang'] = $lang;
  716. }
  717. /**
  718. * Do bulk-update/save of Conditions to Relevance
  719. * @param <integer> $surveyId - if NULL, processes the entire database, otherwise just the specified survey
  720. * @param <integer> $qid - if specified, just updates that one question
  721. * @return array of query strings
  722. */
  723. public static function UpgradeConditionsToRelevance($surveyId=NULL, $qid=NULL)
  724. {
  725. LimeExpressionManager::SetDirtyFlag(); // set dirty flag even if not conditions, since must have had a DB change
  726. // Cheat and upgrade question attributes here too.
  727. self::UpgradeQuestionAttributes(true,$surveyId,$qid);
  728. $releqns = self::ConvertConditionsToRelevance($surveyId,$qid);
  729. $num = count($releqns);
  730. if ($num == 0) {
  731. return NULL;
  732. }
  733. $queries = array();
  734. foreach ($releqns as $key=>$value) {
  735. $query = "UPDATE {{questions}} SET relevance=".Yii::app()->db->quoteValue($value)." WHERE qid=".$key;
  736. dbExecuteAssoc($query);
  737. $queries[] = $query;
  738. }
  739. LimeExpressionManager::SetDirtyFlag();
  740. return $queries;
  741. }
  742. /**
  743. * This reverses UpgradeConditionsToRelevance(). It removes Relevance for questions that have Conditions
  744. * @param <integer> $surveyId
  745. * @param <integer> $qid
  746. */
  747. public static function RevertUpgradeConditionsToRelevance($surveyId=NULL, $qid=NULL)
  748. {
  749. LimeExpressionManager::SetDirtyFlag(); // set dirty flag even if not conditions, since must have had a DB change
  750. $releqns = self::ConvertConditionsToRelevance($surveyId,$qid);
  751. $num = count($releqns);
  752. if ($num == 0) {
  753. return NULL;
  754. }
  755. foreach ($releqns as $key=>$value) {
  756. $query = "UPDATE {{questions}} SET relevance=1 WHERE qid=".$key;
  757. dbExecuteAssoc($query);
  758. }
  759. return count($releqns);
  760. }
  761. /**
  762. * If $qid is set, returns the relevance equation generated from conditions (or NULL if there are no conditions for that $qid)
  763. * If $qid is NULL, returns an array of relevance equations generated from Conditions, keyed on the question ID
  764. * @param <integer> $surveyId
  765. * @param <integer> $qid - if passed, only generates relevance equation for that question - otherwise genereates for all questions with conditions
  766. * @return array of generated relevance strings, indexed by $qid
  767. */
  768. public static function ConvertConditionsToRelevance($surveyId=NULL, $qid=NULL)
  769. {
  770. $query = LimeExpressionManager::getConditionsForEM($surveyId,$qid);
  771. $_qid = -1;
  772. $relevanceEqns = array();
  773. $scenarios = array();
  774. $relAndList = array();
  775. $relOrList = array();
  776. foreach($query->readAll() as $row)
  777. {
  778. $row['method']=trim($row['method']); //For Postgres
  779. if ($row['qid'] != $_qid)
  780. {
  781. // output the values for prior question is there was one
  782. if ($_qid != -1)
  783. {
  784. if (count($relOrList) > 0)
  785. {
  786. $relAndList[] = '(' . implode(' or ', $relOrList) . ')';
  787. }
  788. if (count($relAndList) > 0)
  789. {
  790. $scenarios[] = '(' . implode(' and ', $relAndList) . ')';
  791. }
  792. $relevanceEqn = implode(' or ', $scenarios);
  793. $relevanceEqns[$_qid] = $relevanceEqn;
  794. }
  795. // clear for next question
  796. $_qid = $row['qid'];
  797. $_scenario = $row['scenario'];
  798. $_cqid = $row['cqid'];
  799. $_subqid = -1;
  800. $relAndList = array();
  801. $relOrList = array();
  802. $scenarios = array();
  803. $releqn = '';
  804. }
  805. if ($row['scenario'] != $_scenario)
  806. {
  807. if (count($relOrList) > 0)
  808. {
  809. $relAndList[] = '(' . implode(' or ', $relOrList) . ')';
  810. }
  811. $scenarios[] = '(' . implode(' and ', $relAndList) . ')';
  812. $relAndList = array();
  813. $relOrList = array();
  814. $_scenario = $row['scenario'];
  815. $_cqid = $row['cqid'];
  816. $_subqid = -1;
  817. }
  818. if ($row['cqid'] != $_cqid)
  819. {
  820. $relAndList[] = '(' . implode(' or ', $relOrList) . ')';
  821. $relOrList = array();
  822. $_cqid = $row['cqid'];
  823. $_subqid = -1;
  824. }
  825. // fix fieldnames
  826. if ($row['type'] == '' && preg_match('/^{.+}$/',$row['cfieldname'])) {
  827. $fieldname = substr($row['cfieldname'],1,-1); // {TOKEN:xxxx}
  828. $subqid = $fieldname;
  829. $value = $row['value'];
  830. }
  831. else if ($row['type'] == 'M' || $row['type'] == 'P') {
  832. if (substr($row['cfieldname'],0,1) == '+') {
  833. // if prefixed with +, then a fully resolved name
  834. $fieldname = substr($row['cfieldname'],1) . '.NAOK';
  835. $subqid = $fieldname;
  836. $value = $row['value'];
  837. }
  838. else {
  839. // else create name by concatenating two parts together
  840. $fieldname = $row['cfieldname'] . $row['value'] . '.NAOK';
  841. $subqid = $row['cfieldname'];
  842. $value = 'Y';
  843. }
  844. }
  845. else {
  846. $fieldname = $row['cfieldname'] . '.NAOK';
  847. $subqid = $fieldname;
  848. $value = $row['value'];
  849. }
  850. if ($_subqid != -1 && $_subqid != $subqid)
  851. {
  852. $relAndList[] = '(' . implode(' or ', $relOrList) . ')';
  853. $relOrList = array();
  854. }
  855. $_subqid = $subqid;
  856. // fix values
  857. if (preg_match('/^@\d+X\d+X\d+.*@$/',$value)) {
  858. $value = substr($value,1,-1);
  859. }
  860. else if (preg_match('/^{.+}$/',$value)) {
  861. $value = substr($value,1,-1);
  862. }
  863. else if ($row['method'] == 'RX') {
  864. if (!preg_match('#^/.*/$#',$value))
  865. {
  866. $value = '"/' . $value . '/"'; // if not surrounded by slashes, add them.
  867. }
  868. }
  869. else {
  870. $value = '"' . $value . '"';
  871. }
  872. // add equation
  873. if ($row['method'] == 'RX')
  874. {
  875. $relOrList[] = "regexMatch(" . $value . "," . $fieldname . ")";
  876. }
  877. else
  878. {
  879. // Conditions uses ' ' to mean not answered, but internally it is really stored as ''. Fix this
  880. if ($value === '" "' || $value == '""') {
  881. if ($row['method'] == '==')
  882. {
  883. $relOrList[] = "is_empty(" . $fieldname . ")";
  884. }
  885. else if ($row['method'] == '!=')
  886. {
  887. $relOrList[] = "!is_empty(" . $fieldname . ")";
  888. }
  889. else
  890. {
  891. $relOrList[] = $fieldname . " " . $row['method'] . " " . $value;
  892. }
  893. }
  894. else
  895. {
  896. if ($value == '"0"' || !preg_match('/^".+"$/',$value))
  897. {
  898. switch ($row['method'])
  899. {
  900. case '==':
  901. case '<':
  902. case '<=':
  903. case '>=':
  904. $relOrList[] = '(!is_empty(' . $fieldname . ') && (' . $fieldname . " " . $row['method'] . " " . $value . '))';
  905. break;
  906. case '!=':
  907. $relOrList[] = '(is_empty(' . $fieldname . ') || (' . $fieldname . " != " . $value . '))';
  908. break;
  909. default:
  910. $relOrList[] = $fieldname . " " . $row['method'] . " " . $value;
  911. break;
  912. }
  913. }
  914. else
  915. {
  916. switch ($row['method'])
  917. {
  918. case '<':
  919. case '<=':
  920. $relOrList[] = '(!is_empty(' . $fieldname . ') && (' . $fieldname . " " . $row['method'] . " " . $value . '))';
  921. break;
  922. default:
  923. $relOrList[] = $fieldname . " " . $row['method'] . " " . $value;
  924. break;
  925. }
  926. }
  927. }
  928. }
  929. if ($row['cqid'] == 0 || substr($row['cfieldname'],0,1) == '+') {
  930. $_cqid = -1; // forces this statement to be ANDed instead of being part of a cqid OR group
  931. }
  932. }
  933. // output last one
  934. if ($_qid != -1)
  935. {
  936. if (count($relOrList) > 0)
  937. {
  938. $relAndList[] = '(' . implode(' or ', $relOrList) . ')';
  939. }
  940. if (count($relAndList) > 0)
  941. {
  942. $scenarios[] = '(' . implode(' and ', $relAndList) . ')';
  943. }
  944. $relevanceEqn = implode(' or ', $scenarios);
  945. $relevanceEqns[$_qid] = $relevanceEqn;
  946. }
  947. if (is_null($qid)) {
  948. return $relevanceEqns;
  949. }
  950. else {
  951. if (isset($relevanceEqns[$qid]))
  952. {
  953. $result = array();
  954. $result[$qid] = $relevanceEqns[$qid];
  955. return $result;
  956. }
  957. else
  958. {
  959. return NULL;
  960. }
  961. }
  962. }
  963. /**
  964. * Return list of relevance equations generated from conditions
  965. * @param <integer> $surveyId
  966. * @param <integer> $qid
  967. * @return array of relevance equations, indexed by $qid
  968. */
  969. public static function UnitTestConvertConditionsToRelevance($surveyId=NULL, $qid=NULL)
  970. {
  971. $LEM =& LimeExpressionManager::singleton();
  972. return $LEM->ConvertConditionsToRelevance($surveyId, $qid);
  973. }
  974. /**
  975. * Process all question attributes that apply to EM
  976. * (1) Sub-question-level relevance: e.g. array_filter, array_filter_exclude
  977. * (2) Validations: e.g. min/max number of answers; min/max/eq sum of answers
  978. * @param <integer> $onlyThisQseq - only process these attributes for the specified question
  979. */
  980. public function _CreateSubQLevelRelevanceAndValidationEqns($onlyThisQseq=NULL)
  981. {
  982. // $now = microtime(true);
  983. $this->subQrelInfo=array(); // reset it each time this is called
  984. $subQrels = array(); // array of sub-question-level relevance equations
  985. $validationEqn = array();
  986. $validationTips = array(); // array of visible tips for validation criteria, indexed by $qid
  987. // Associate these with $qid so that can be nested under appropriate question-level relevance
  988. foreach ($this->q2subqInfo as $qinfo)
  989. {
  990. if (!is_null($onlyThisQseq) && $onlyThisQseq != $qinfo['qseq']) {
  991. continue;
  992. }
  993. else if (!$this->allOnOnePage && $this->currentGroupSeq != $qinfo['gseq']) {
  994. continue; // only need subq relevance for current page.
  995. }
  996. $questionNum = $qinfo['qid'];
  997. $type = $qinfo['type'];
  998. $hasSubqs = (isset($qinfo['subqs']) && count($qinfo['subqs'] > 0));
  999. $qattr = isset($this->qattr[$questionNum]) ? $this->qattr[$questionNum] : array();
  1000. if (isset($qattr['input_boxes']) && $qattr['input_boxes'] == '1')
  1001. {
  1002. $input_boxes='1';
  1003. }
  1004. else
  1005. {
  1006. $input_boxes='';
  1007. }
  1008. if (isset($qattr['value_range_allows_missing']) && $qattr['value_range_allows_missing'] == '1')
  1009. {
  1010. $value_range_allows_missing = true;
  1011. }
  1012. else
  1013. {
  1014. $value_range_allows_missing = false;
  1015. }
  1016. // array_filter
  1017. // If want to filter question Q2 on Q1, where each have subquestions SQ1-SQ3, this is equivalent to relevance equations of:
  1018. // relevance for Q2_SQ1 is Q1_SQ1!=''
  1019. $array_filter = NULL;
  1020. if (isset($qattr['array_filter']) && trim($qattr['array_filter']) != '')
  1021. {
  1022. $array_filter = $qattr['array_filter'];
  1023. $this->qrootVarName2arrayFilter[$qinfo['rootVarName']]['array_filter'] = $array_filter;
  1024. }
  1025. // array_filter_exclude
  1026. // If want to filter question Q2 on Q1, where each have subquestions SQ1-SQ3, this is equivalent to relevance equations of:
  1027. // relevance for Q2_SQ1 is Q1_SQ1==''
  1028. $array_filter_exclude = NULL;
  1029. if (isset($qattr['array_filter_exclude']) && trim($qattr['array_filter_exclude']) != '')
  1030. {
  1031. $array_filter_exclude = $qattr['array_filter_exclude'];
  1032. $this->qrootVarName2arrayFilter[$qinfo['rootVarName']]['array_filter_exclude'] = $array_filter_exclude;
  1033. }
  1034. // array_filter and array_filter_exclude get processed together
  1035. if (!is_null($array_filter) || !is_null($array_filter_exclude))
  1036. {
  1037. if ($hasSubqs) {
  1038. $cascadedAF = array();
  1039. $cascadedAFE = array();
  1040. list($cascadedAF, $cascadedAFE) = $this->_recursivelyFindAntecdentArrayFilters($qinfo['rootVarName'],array(),array());
  1041. $cascadedAF = array_reverse($cascadedAF);
  1042. $cascadedAFE = array_reverse($cascadedAFE);
  1043. $subqs = $qinfo['subqs'];
  1044. if ($type == 'R') {
  1045. $subqs = array();
  1046. foreach ($this->qans[$qinfo['qid']] as $k=>$v)
  1047. {
  1048. $_code = explode('~',$k);
  1049. $subqs[] = array(
  1050. 'rowdivid'=>$qinfo['sgqa'] . $_code[1],
  1051. 'sqsuffix'=>'_' . $_code[1],
  1052. );
  1053. }
  1054. }
  1055. $last_rowdivid = '--';
  1056. foreach ($subqs as $sq) {
  1057. if ($sq['rowdivid'] == $last_rowdivid)
  1058. {
  1059. continue;
  1060. }
  1061. $last_rowdivid = $sq['rowdivid'];
  1062. $af_names = array();
  1063. $afe_names = array();
  1064. switch ($type)
  1065. {
  1066. case '1': //Array (Flexible Labels) dual scale
  1067. case ':': //ARRAY (Multi Flexi) 1 to 10
  1068. case ';': //ARRAY (Multi Flexi) Text
  1069. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  1070. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  1071. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  1072. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  1073. case 'F': //ARRAY (Flexible) - Row Format
  1074. case 'L': //LIST drop-down/radio-button list
  1075. case 'M': //Multiple choice checkbox
  1076. case 'P': //Multiple choice with comments checkbox + text
  1077. case 'K': //MULTIPLE NUMERICAL QUESTION
  1078. case 'Q': //MULTIPLE SHORT TEXT
  1079. case 'R': //Ranking
  1080. // if ($this->sgqaNaming)
  1081. // {
  1082. foreach ($cascadedAF as $_caf)
  1083. {
  1084. $sgq = ((isset($this->qcode2sgq[$_caf])) ? $this->qcode2sgq[$_caf] : $_caf);
  1085. $fqid = explode('X',$sgq);
  1086. if (!isset($fqid[2]))
  1087. {
  1088. continue;
  1089. }
  1090. $fqid = $fqid[2];
  1091. if ($this->q2subqInfo[$fqid]['type'] == 'R')
  1092. {
  1093. $rankables = array();
  1094. foreach ($this->qans[$fqid] as $k=>$v)
  1095. {
  1096. $rankable = explode('~',$k);
  1097. $rankables[] = '_' . $rankable[1];
  1098. }
  1099. if (array_search($sq['sqsuffix'],$rankables) === false)
  1100. {
  1101. continue;
  1102. }
  1103. }
  1104. $fsqs = array();
  1105. foreach ($this->q2subqInfo[$fqid]['subqs'] as $fsq)
  1106. {
  1107. if ($this->q2subqInfo[$fqid]['type'] == 'R')
  1108. {
  1109. // we know the suffix exists
  1110. $fsqs[] = '(' . $sgq . $fsq['csuffix'] . ".NAOK == '" . substr($sq['sqsuffix'],1) . "')";
  1111. }
  1112. else if ($this->q2subqInfo[$fqid]['type'] == ':' && isset($this->qattr[$fqid]['multiflexible_checkbox']) && $this->qattr[$fqid]['multiflexible_checkbox']=='1')
  1113. {
  1114. if ($fsq['sqsuffix'] == $sq['sqsuffix'])
  1115. {
  1116. $fsqs[] = $sgq . $fsq['csuffix'] . '.NAOK=="1"';
  1117. }
  1118. }
  1119. else
  1120. {
  1121. if ($fsq['sqsuffix'] == $sq['sqsuffix'])
  1122. {
  1123. $fsqs[] = '!is_empty(' . $sgq . $fsq['csuffix'] . '.NAOK)';
  1124. }
  1125. }
  1126. }
  1127. if (count($fsqs) > 0)
  1128. {
  1129. $af_names[] = '(' . implode(' or ', $fsqs) . ')';
  1130. }
  1131. }
  1132. foreach ($cascadedAFE as $_cafe)
  1133. {
  1134. $sgq = ((isset($this->qcode2sgq[$_cafe])) ? $this->qcode2sgq[$_cafe] : $_cafe);
  1135. $fqid = explode('X',$sgq);
  1136. if (!isset($fqid[2]))
  1137. {
  1138. continue;
  1139. }
  1140. $fqid = $fqid[2];
  1141. if ($this->q2subqInfo[$fqid]['type'] == 'R')
  1142. {
  1143. $rankables = array();
  1144. foreach ($this->qans[$fqid] as $k=>$v)
  1145. {
  1146. $rankable = explode('~',$k);
  1147. $rankables[] = '_' . $rankable[1];
  1148. }
  1149. if (array_search($sq['sqsuffix'],$rankables) === false)
  1150. {
  1151. continue;
  1152. }
  1153. }
  1154. $fsqs = array();
  1155. foreach ($this->q2subqInfo[$fqid]['subqs'] as $fsq)
  1156. {
  1157. if ($this->q2subqInfo[$fqid]['type'] == 'R')
  1158. {
  1159. // we know the suffix exists
  1160. $fsqs[] = '(' . $sgq . $fsq['csuffix'] . ".NAOK != '" . substr($sq['sqsuffix'],1) . "')";
  1161. }
  1162. else if ($this->q2subqInfo[$fqid]['type'] == ':' && isset($this->qattr[$fqid]['multiflexible_checkbox']) && $this->qattr[$fqid]['multiflexible_checkbox']=='1')
  1163. {
  1164. if ($fsq['sqsuffix'] == $sq['sqsuffix'])
  1165. {
  1166. $fsqs[] = $sgq . $fsq['csuffix'] . '.NAOK!="1"';
  1167. }
  1168. }
  1169. else
  1170. {
  1171. if ($fsq['sqsuffix'] == $sq['sqsuffix'])
  1172. {
  1173. $fsqs[] = 'is_empty(' . $sgq . $fsq['csuffix'] . '.NAOK)';
  1174. }
  1175. }
  1176. }
  1177. if (count($fsqs) > 0)
  1178. {
  1179. $afe_names[] = '(' . implode(' and ', $fsqs) . ')';
  1180. }
  1181. }
  1182. // }
  1183. // else // TODO - implement qcode naming for this
  1184. // {
  1185. // foreach ($cascadedAF as $_caf)
  1186. // {
  1187. // $sgq = $_caf . $sq['sqsuffix'];
  1188. // if (isset($this->knownVars[$sgq]))
  1189. // {
  1190. // $af_names[] = $sgq . '.NAOK';
  1191. // }
  1192. // }
  1193. // foreach ($cascadedAFE as $_cafe)
  1194. // {
  1195. // $sgq = $_cafe . $sq['sqsuffix'];
  1196. // if (isset($this->knownVars[$sgq]))
  1197. // {
  1198. // $afe_names[] = $sgq . '.NAOK';
  1199. // }
  1200. // }
  1201. // }
  1202. break;
  1203. default:
  1204. break;
  1205. }
  1206. $af_names = array_unique($af_names);
  1207. $afe_names= array_unique($afe_names);
  1208. if (count($af_names) > 0 || count($afe_names) > 0) {
  1209. $afs_eqn = '';
  1210. if (count($af_names) > 0)
  1211. {
  1212. $afs_eqn .= implode(' && ', $af_names);
  1213. }
  1214. if (count($afe_names) > 0)
  1215. {
  1216. if ($afs_eqn != '')
  1217. {
  1218. $afs_eqn .= ' && ';
  1219. }
  1220. $afs_eqn .= implode(' && ', $afe_names);
  1221. }
  1222. $subQrels[] = array(
  1223. 'qtype' => $type,
  1224. 'type' => 'array_filter',
  1225. 'rowdivid' => $sq['rowdivid'],
  1226. 'eqn' => '(' . $afs_eqn . ')',
  1227. 'qid' => $questionNum,
  1228. 'sgqa' => $qinfo['sgqa'],
  1229. );
  1230. }
  1231. }
  1232. }
  1233. }
  1234. // code_filter: WZ
  1235. // This can be skipped, since question types 'W' (list-dropdown-flexible) and 'Z'(list-radio-flexible) are no longer supported
  1236. // equals_num_value
  1237. // Validation:= sum(sq1,...,sqN) == value (which could be an expression).
  1238. if (isset($qattr['equals_num_value']) && trim($qattr['equals_num_value']) != '')
  1239. {
  1240. $equals_num_value = $qattr['equals_num_value'];
  1241. if ($hasSubqs) {
  1242. $subqs = $qinfo['subqs'];
  1243. $sq_names = array();
  1244. foreach ($subqs as $sq) {
  1245. $sq_name = NULL;
  1246. switch ($type)
  1247. {
  1248. case 'K': //MULTIPLE NUMERICAL QUESTION
  1249. if ($this->sgqaNaming)
  1250. {
  1251. $sq_name = $sq['rowdivid'] . '.NAOK';
  1252. }
  1253. else
  1254. {
  1255. $sq_name = $sq['varName'] . '.NAOK';
  1256. }
  1257. break;
  1258. default:
  1259. break;
  1260. }
  1261. if (!is_null($sq_name)) {
  1262. $sq_names[] = $sq_name;
  1263. }
  1264. }
  1265. if (count($sq_names) > 0) {
  1266. if (!isset($validationEqn[$questionNum]))
  1267. {
  1268. $validationEqn[$questionNum] = array();
  1269. }
  1270. // sumEqn and sumRemainingEqn may need to be rounded if using sliders
  1271. $precision=LEM_DEFAULT_PRECISION; // default is not to round
  1272. if (isset($qattr['slider_layout']) && $qattr['slider_layout']=='1')
  1273. {
  1274. $precision=0; // default is to round to whole numbers
  1275. if (isset($qattr['slider_accuracy']) && trim($qattr['slider_accuracy'])!='')
  1276. {
  1277. $slider_accuracy = $qattr['slider_accuracy'];
  1278. $_parts = explode('.',$slider_accuracy);
  1279. if (isset($_parts[1]))
  1280. {
  1281. $precision = strlen($_parts[1]); // number of digits after mantissa
  1282. }
  1283. }
  1284. }
  1285. $sumEqn = 'sum(' . implode(', ', $sq_names) . ')';
  1286. $sumRemainingEqn = '(' . $equals_num_value . ' - sum(' . implode(', ', $sq_names) . '))';
  1287. $mainEqn = 'sum(' . implode(', ', $sq_names) . ')';
  1288. if (!is_null($precision))
  1289. {
  1290. $sumEqn = 'round(' . $sumEqn . ', ' . $precision . ')';
  1291. $sumRemainingEqn = 'round(' . $sumRemainingEqn . ', ' . $precision . ')';
  1292. $mainEqn = 'round(' . $mainEqn . ', ' . $precision . ')';
  1293. }
  1294. $noanswer_option = '';
  1295. if ($value_range_allows_missing)
  1296. {
  1297. $noanswer_option = ' || count(' . implode(', ', $sq_names) . ') == 0';
  1298. }
  1299. $validationEqn[$questionNum][] = array(
  1300. 'qtype' => $type,
  1301. 'type' => 'equals_num_value',
  1302. 'class' => 'sum_range',
  1303. 'eqn' => ($qinfo['mandatory']=='Y')?'(' . $mainEqn . ' == (' . $equals_num_value . '))':'(' . $mainEqn . ' == (' . $equals_num_value . ')' . $noanswer_option . ')',
  1304. 'qid' => $questionNum,
  1305. 'sumEqn' => $sumEqn,
  1306. 'sumRemainingEqn' => $sumRemainingEqn,
  1307. );
  1308. }
  1309. }
  1310. }
  1311. else
  1312. {
  1313. $equals_num_value='';
  1314. }
  1315. // exclude_all_others
  1316. // If any excluded options are true (and relevant), then disable all other input elements for that question
  1317. if (isset($qattr['exclude_all_others']) && trim($qattr['exclude_all_others']) != '')
  1318. {
  1319. $exclusive_options = explode(';',$qattr['exclude_all_others']);
  1320. if ($hasSubqs) {
  1321. foreach ($exclusive_options as $exclusive_option)
  1322. {
  1323. $exclusive_option = trim($exclusive_option);
  1324. if ($exclusive_option == '') {
  1325. continue;
  1326. }
  1327. $subqs = $qinfo['subqs'];
  1328. $sq_names = array();
  1329. foreach ($subqs as $sq) {
  1330. $sq_name = NULL;
  1331. if ($sq['csuffix'] == $exclusive_option)
  1332. {
  1333. continue; // so don't make the excluded option irrelevant
  1334. }
  1335. switch ($type)
  1336. {
  1337. case ':': //ARRAY (Multi Flexi) 1 to 10
  1338. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  1339. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  1340. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  1341. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  1342. case 'F': //ARRAY (Flexible) - Row Format
  1343. case 'M': //Multiple choice checkbox
  1344. case 'P': //Multiple choice with comments checkbox + text
  1345. case 'K': //MULTIPLE NUMERICAL QUESTION
  1346. case 'Q': //MULTIPLE SHORT TEXT
  1347. if ($this->sgqaNaming)
  1348. {
  1349. $sq_name = $qinfo['sgqa'] . trim($exclusive_option) . '.NAOK';
  1350. }
  1351. else
  1352. {
  1353. $sq_name = $qinfo['sgqa'] . trim($exclusive_option) . '.NAOK';
  1354. }
  1355. break;
  1356. default:
  1357. break;
  1358. }
  1359. if (!is_null($sq_name)) {
  1360. $subQrels[] = array(
  1361. 'qtype' => $type,
  1362. 'type' => 'exclude_all_others',
  1363. 'rowdivid' => $sq['rowdivid'],
  1364. 'eqn' => 'is_empty(' . $sq_name . ')',
  1365. 'qid' => $questionNum,
  1366. 'sgqa' => $qinfo['sgqa'],
  1367. );
  1368. }
  1369. }
  1370. }
  1371. }
  1372. }
  1373. else
  1374. {
  1375. $exclusive_option = '';
  1376. }
  1377. // exclude_all_others_auto
  1378. // if (count(this.relevanceStatus) == count(this)) { set exclusive option value to "Y" and call checkconditions() }
  1379. // 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
  1380. if (isset($qattr['exclude_all_others_auto']) && trim($qattr['exclude_all_others_auto']) == '1'
  1381. && isset($qattr['exclude_all_others']) && trim($qattr['exclude_all_others']) != '' && count(explode(';',trim($qattr['exclude_all_others']))) == 1)
  1382. {
  1383. $exclusive_option = trim($qattr['exclude_all_others']);
  1384. if ($hasSubqs) {
  1385. $subqs = $qinfo['subqs'];
  1386. $sq_names = array();
  1387. foreach ($subqs as $sq) {
  1388. $sq_name = NULL;
  1389. switch ($type)
  1390. {
  1391. case 'M': //Multiple choice checkbox
  1392. case 'P': //Multiple choice with comments checkbox + text
  1393. if ($this->sgqaNaming)
  1394. {
  1395. $sq_name = substr($sq['jsVarName'],4);
  1396. }
  1397. else
  1398. {
  1399. $sq_name = $sq['varName'];
  1400. }
  1401. break;
  1402. default:
  1403. break;
  1404. }
  1405. if (!is_null($sq_name))
  1406. {
  1407. if ($sq['csuffix'] == $exclusive_option)
  1408. {
  1409. $eoVarName = substr($sq['jsVarName'],4);
  1410. }
  1411. else
  1412. {
  1413. $sq_names[] = $sq_name;
  1414. }
  1415. }
  1416. }
  1417. if (count($sq_names) > 0) {
  1418. $relpart = "sum(" . implode(".relevanceStatus, ", $sq_names) . ".relevanceStatus)";
  1419. $checkedpart = "count(" . implode(".NAOK, ", $sq_names) . ".NAOK)";
  1420. $eoRelevantAndUnchecked = "(" . $eoVarName . ".relevanceStatus && is_empty(" . $eoVarName . "))";
  1421. $eoEqn = "(" . $eoRelevantAndUnchecked . " && (" . $relpart . " == " . $checkedpart . "))";
  1422. $this->em->ProcessBooleanExpression($eoEqn, $qinfo['gseq'], $qinfo['qseq']);
  1423. $relevanceVars = implode('|',$this->em->GetJSVarsUsed());
  1424. $relevanceJS = $this->em->GetJavaScriptEquivalentOfExpression();
  1425. // Unset all checkboxes and hidden values for this question (irregardless of whether they are array filtered)
  1426. $eosaJS = "if (" . $relevanceJS . ") {\n";
  1427. $eosaJS .=" $('#question" . $questionNum . " [type=checkbox]').attr('checked',false);\n";
  1428. $eosaJS .=" $('#java" . $qinfo['sgqa'] . "other').val('');\n";
  1429. $eosaJS .=" $('#answer" . $qinfo['sgqa'] . "other').val('');\n";
  1430. $eosaJS .=" $('[id^=java" . $qinfo['sgqa'] . "]').val('');\n";
  1431. $eosaJS .=" $('#answer" . $eoVarName . "').attr('checked',true);\n";
  1432. $eosaJS .=" $('#java" . $eoVarName . "').val('Y');\n";
  1433. $eosaJS .=" LEMrel" . $questionNum . "();\n";
  1434. $eosaJS .=" relChange" . $questionNum ."=true;\n";
  1435. $eosaJS .="}\n";
  1436. $this->qid2exclusiveAuto[$questionNum] = array(
  1437. 'js'=>$eosaJS,
  1438. 'relevanceVars'=>$relevanceVars, // so that EM knows which variables to declare
  1439. 'rowdivid'=>$eoVarName, // to ensure that EM creates a hidden relevanceSGQA input for the exclusive option
  1440. );
  1441. }
  1442. }
  1443. }
  1444. // min_answers
  1445. // Validation:= count(sq1,...,sqN) >= value (which could be an expression).
  1446. if (isset($qattr['min_answers']) && trim($qattr['min_answers']) != '')
  1447. {
  1448. $min_answers = $qattr['min_answers'];
  1449. if ($hasSubqs) {
  1450. $subqs = $qinfo['subqs'];
  1451. $sq_names = array();
  1452. foreach ($subqs as $sq) {
  1453. $sq_name = NULL;
  1454. switch ($type)
  1455. {
  1456. case '1': //Array (Flexible Labels) dual scale
  1457. if (substr($sq['varName'],-1,1) == '0')
  1458. {
  1459. if ($this->sgqaNaming)
  1460. {
  1461. $base = substr(substr($sq['jsVarName'],4),0,-1);
  1462. $sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
  1463. }
  1464. else
  1465. {
  1466. $base = substr($sq['varName'],0,-1);
  1467. $sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
  1468. }
  1469. }
  1470. break;
  1471. case ':': //ARRAY (Multi Flexi) 1 to 10
  1472. case ';': //ARRAY (Multi Flexi) Text
  1473. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  1474. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  1475. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  1476. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  1477. case 'F': //ARRAY (Flexible) - Row Format
  1478. case 'K': //MULTIPLE NUMERICAL QUESTION
  1479. case 'Q': //MULTIPLE SHORT TEXT
  1480. case 'M': //Multiple choice checkbox
  1481. case 'R': //RANKING STYLE
  1482. if ($this->sgqaNaming)
  1483. {
  1484. $sq_name = substr($sq['jsVarName'],4) . '.NAOK';
  1485. }
  1486. else
  1487. {
  1488. $sq_name = $sq['varName'] . '.NAOK';
  1489. }
  1490. break;
  1491. case 'P': //Multiple choice with comments checkbox + text
  1492. if (!preg_match('/comment$/',$sq['varName'])) {
  1493. if ($this->sgqaNaming)
  1494. {
  1495. $sq_name = $sq['rowdivid'] . '.NAOK';
  1496. }
  1497. else
  1498. {
  1499. $sq_name = $sq['rowdivid'] . '.NAOK';
  1500. }
  1501. }
  1502. break;
  1503. default:
  1504. break;
  1505. }
  1506. if (!is_null($sq_name)) {
  1507. $sq_names[] = $sq_name;
  1508. }
  1509. }
  1510. if (count($sq_names) > 0) {
  1511. if (!isset($validationEqn[$questionNum]))
  1512. {
  1513. $validationEqn[$questionNum] = array();
  1514. }
  1515. $validationEqn[$questionNum][] = array(
  1516. 'qtype' => $type,
  1517. 'type' => 'min_answers',
  1518. 'class' => 'num_answers',
  1519. 'eqn' => 'if(is_empty('.$min_answers.'),1,(count(' . implode(', ', $sq_names) . ') >= (' . $min_answers . ')))',
  1520. 'qid' => $questionNum,
  1521. );
  1522. }
  1523. }
  1524. }
  1525. else
  1526. {
  1527. $min_answers='';
  1528. }
  1529. // max_answers
  1530. // Validation:= count(sq1,...,sqN) <= value (which could be an expression).
  1531. if (isset($qattr['max_answers']) && trim($qattr['max_answers']) != '')
  1532. {
  1533. $max_answers = $qattr['max_answers'];
  1534. if ($hasSubqs) {
  1535. $subqs = $qinfo['subqs'];
  1536. $sq_names = array();
  1537. foreach ($subqs as $sq) {
  1538. $sq_name = NULL;
  1539. switch ($type)
  1540. {
  1541. case '1': //Array (Flexible Labels) dual scale
  1542. if (substr($sq['varName'],-1,1) == '0')
  1543. {
  1544. if ($this->sgqaNaming)
  1545. {
  1546. $base = substr(substr($sq['jsVarName'],4),0,-1);
  1547. $sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
  1548. }
  1549. else
  1550. {
  1551. $base = substr($sq['varName'],0,-1);
  1552. $sq_name = "if(count(" . $base . "0.NAOK," . $base . "1.NAOK)==2,1,'')";
  1553. }
  1554. }
  1555. break;
  1556. case ':': //ARRAY (Multi Flexi) 1 to 10
  1557. case ';': //ARRAY (Multi Flexi) Text
  1558. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  1559. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  1560. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  1561. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  1562. case 'F': //ARRAY (Flexible) - Row Format
  1563. case 'K': //MULTIPLE NUMERICAL QUESTION
  1564. case 'Q': //MULTIPLE SHORT TEXT
  1565. case 'M': //Multiple choice checkbox
  1566. case 'R': //RANKING STYLE
  1567. if ($this->sgqaNaming)
  1568. {
  1569. $sq_name = substr($sq['jsVarName'],4) . '.NAOK';
  1570. }
  1571. else
  1572. {
  1573. $sq_name = $sq['varName'] . '.NAOK';
  1574. }
  1575. break;
  1576. case 'P': //Multiple choice with comments checkbox + text
  1577. if (!preg_match('/comment$/',$sq['varName'])) {
  1578. if ($this->sgqaNaming)
  1579. {
  1580. $sq_name = $sq['rowdivid'] . '.NAOK';
  1581. }
  1582. else
  1583. {
  1584. $sq_name = $sq['varName'] . '.NAOK';
  1585. }
  1586. }
  1587. break;
  1588. default:
  1589. break;
  1590. }
  1591. if (!is_null($sq_name)) {
  1592. $sq_names[] = $sq_name;
  1593. }
  1594. }
  1595. if (count($sq_names) > 0) {
  1596. if (!isset($validationEqn[$questionNum]))
  1597. {
  1598. $validationEqn[$questionNum] = array();
  1599. }
  1600. $validationEqn[$questionNum][] = array(
  1601. 'qtype' => $type,
  1602. 'type' => 'max_answers',
  1603. 'class' => 'num_answers',
  1604. 'eqn' => '(if(is_empty('.$max_answers.'),1,count(' . implode(', ', $sq_names) . ') <= (' . $max_answers . ')))',
  1605. 'qid' => $questionNum,
  1606. );
  1607. }
  1608. }
  1609. }
  1610. else
  1611. {
  1612. $max_answers='';
  1613. }
  1614. // min_num_value_n
  1615. // Validation:= N >= value (which could be an expression).
  1616. if (isset($qattr['min_num_value_n']) && trim($qattr['min_num_value_n']) != '')
  1617. {
  1618. $min_num_value_n = $qattr['min_num_value_n'];
  1619. if ($hasSubqs) {
  1620. $subqs = $qinfo['subqs'];
  1621. $sq_names = array();
  1622. $subqValidEqns = array();
  1623. foreach ($subqs as $sq) {
  1624. $sq_name = NULL;
  1625. switch ($type)
  1626. {
  1627. case 'K': //MULTIPLE NUMERICAL QUESTION
  1628. if ($this->sgqaNaming)
  1629. {
  1630. if(($qinfo['mandatory']=='Y')){
  1631. $sq_name = '('. $sq['rowdivid'] . '.NAOK >= (' . $min_num_value_n . '))';
  1632. }else{
  1633. $sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK >= (' . $min_num_value_n . '))';
  1634. }
  1635. }
  1636. else
  1637. {
  1638. if(($qinfo['mandatory']=='Y')){
  1639. $sq_name = '('. $sq['varName'] . '.NAOK >= (' . $min_num_value_n . '))';
  1640. }else{
  1641. $sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK >= (' . $min_num_value_n . '))';
  1642. }
  1643. }
  1644. $subqValidSelector = $sq['jsVarName_on'];
  1645. break;
  1646. case 'N': //NUMERICAL QUESTION TYPE
  1647. if ($this->sgqaNaming)
  1648. {
  1649. if(($qinfo['mandatory']=='Y')){
  1650. $sq_name = '('. $sq['rowdivid'] . '.NAOK >= (' . $min_num_value_n . '))';
  1651. }else{
  1652. $sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK >= (' . $min_num_value_n . '))';
  1653. }
  1654. }
  1655. else
  1656. {
  1657. if(($qinfo['mandatory']=='Y')){
  1658. $sq_name = '('. $sq['varName'] . '.NAOK >= (' . $min_num_value_n . '))';
  1659. }else{
  1660. $sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK >= (' . $min_num_value_n . '))';
  1661. }
  1662. }
  1663. $subqValidSelector = '';
  1664. break;
  1665. default:
  1666. break;
  1667. }
  1668. if (!is_null($sq_name)) {
  1669. $sq_names[] = $sq_name;
  1670. $subqValidEqns[$subqValidSelector] = array(
  1671. 'subqValidEqn' => $sq_name,
  1672. 'subqValidSelector' => $subqValidSelector,
  1673. );
  1674. }
  1675. }
  1676. if (count($sq_names) > 0) {
  1677. if (!isset($validationEqn[$questionNum]))
  1678. {
  1679. $validationEqn[$questionNum] = array();
  1680. }
  1681. $validationEqn[$questionNum][] = array(
  1682. 'qtype' => $type,
  1683. 'type' => 'min_num_value_n',
  1684. 'class' => 'value_range',
  1685. 'eqn' => implode(' && ', $sq_names),
  1686. 'qid' => $questionNum,
  1687. 'subqValidEqns' => $subqValidEqns,
  1688. );
  1689. }
  1690. }
  1691. }
  1692. else
  1693. {
  1694. $min_num_value_n='';
  1695. }
  1696. // max_num_value_n
  1697. // Validation:= N <= value (which could be an expression).
  1698. if (isset($qattr['max_num_value_n']) && trim($qattr['max_num_value_n']) != '')
  1699. {
  1700. $max_num_value_n = $qattr['max_num_value_n'];
  1701. if ($hasSubqs) {
  1702. $subqs = $qinfo['subqs'];
  1703. $sq_names = array();
  1704. $subqValidEqns = array();
  1705. foreach ($subqs as $sq) {
  1706. $sq_name = NULL;
  1707. switch ($type)
  1708. {
  1709. case 'K': //MULTIPLE NUMERICAL QUESTION
  1710. if ($this->sgqaNaming)
  1711. {
  1712. $sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK <= (' . $max_num_value_n . '))';
  1713. }
  1714. else
  1715. {
  1716. $sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK <= (' . $max_num_value_n . '))';
  1717. }
  1718. $subqValidSelector = $sq['jsVarName_on'];
  1719. break;
  1720. case 'N': //NUMERICAL QUESTION TYPE
  1721. if ($this->sgqaNaming)
  1722. {
  1723. $sq_name = '(is_empty(' . $sq['rowdivid'] . '.NAOK) || '. $sq['rowdivid'] . '.NAOK <= (' . $max_num_value_n . '))';
  1724. }
  1725. else
  1726. {
  1727. $sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || '. $sq['varName'] . '.NAOK <= (' . $max_num_value_n . '))';
  1728. }
  1729. $subqValidSelector = '';
  1730. break;
  1731. default:
  1732. break;
  1733. }
  1734. if (!is_null($sq_name)) {
  1735. $sq_names[] = $sq_name;
  1736. $subqValidEqns[$subqValidSelector] = array(
  1737. 'subqValidEqn' => $sq_name,
  1738. 'subqValidSelector' => $subqValidSelector,
  1739. );
  1740. }
  1741. }
  1742. if (count($sq_names) > 0) {
  1743. if (!isset($validationEqn[$questionNum]))
  1744. {
  1745. $validationEqn[$questionNum] = array();
  1746. }
  1747. $validationEqn[$questionNum][] = array(
  1748. 'qtype' => $type,
  1749. 'type' => 'max_num_value_n',
  1750. 'class' => 'value_range',
  1751. 'eqn' => implode(' && ', $sq_names),
  1752. 'qid' => $questionNum,
  1753. 'subqValidEqns' => $subqValidEqns,
  1754. );
  1755. }
  1756. }
  1757. }
  1758. else
  1759. {
  1760. $max_num_value_n='';
  1761. }
  1762. // min_num_value
  1763. // Validation:= sum(sq1,...,sqN) >= value (which could be an expression).
  1764. if (isset($qattr['min_num_value']) && trim($qattr['min_num_value']) != '')
  1765. {
  1766. $min_num_value = $qattr['min_num_value'];
  1767. if ($hasSubqs) {
  1768. $subqs = $qinfo['subqs'];
  1769. $sq_names = array();
  1770. foreach ($subqs as $sq) {
  1771. $sq_name = NULL;
  1772. switch ($type)
  1773. {
  1774. case 'K': //MULTIPLE NUMERICAL QUESTION
  1775. if ($this->sgqaNaming)
  1776. {
  1777. $sq_name = $sq['rowdivid'] . '.NAOK';
  1778. }
  1779. else
  1780. {
  1781. $sq_name = $sq['varName'] . '.NAOK';
  1782. }
  1783. break;
  1784. default:
  1785. break;
  1786. }
  1787. if (!is_null($sq_name)) {
  1788. $sq_names[] = $sq_name;
  1789. }
  1790. }
  1791. if (count($sq_names) > 0) {
  1792. if (!isset($validationEqn[$questionNum]))
  1793. {
  1794. $validationEqn[$questionNum] = array();
  1795. }
  1796. $sumEqn = 'sum(' . implode(', ', $sq_names) . ')';
  1797. $precision = LEM_DEFAULT_PRECISION;
  1798. if (!is_null($precision))
  1799. {
  1800. $sumEqn = 'round(' . $sumEqn . ', ' . $precision . ')';
  1801. }
  1802. $noanswer_option = '';
  1803. if ($value_range_allows_missing)
  1804. {
  1805. $noanswer_option = ' || count(' . implode(', ', $sq_names) . ') == 0';
  1806. }
  1807. $validationEqn[$questionNum][] = array(
  1808. 'qtype' => $type,
  1809. 'type' => 'min_num_value',
  1810. 'class' => 'sum_range',
  1811. 'eqn' => '(sum(' . implode(', ', $sq_names) . ') >= (' . $min_num_value . ')' . $noanswer_option . ')',
  1812. 'qid' => $questionNum,
  1813. 'sumEqn' => $sumEqn,
  1814. );
  1815. }
  1816. }
  1817. }
  1818. else
  1819. {
  1820. $min_num_value='';
  1821. }
  1822. // max_num_value
  1823. // Validation:= sum(sq1,...,sqN) <= value (which could be an expression).
  1824. if (isset($qattr['max_num_value']) && trim($qattr['max_num_value']) != '')
  1825. {
  1826. $max_num_value = $qattr['max_num_value'];
  1827. if ($hasSubqs) {
  1828. $subqs = $qinfo['subqs'];
  1829. $sq_names = array();
  1830. foreach ($subqs as $sq) {
  1831. $sq_name = NULL;
  1832. switch ($type)
  1833. {
  1834. case 'K': //MULTIPLE NUMERICAL QUESTION
  1835. if ($this->sgqaNaming)
  1836. {
  1837. $sq_name = $sq['rowdivid'] . '.NAOK';
  1838. }
  1839. else
  1840. {
  1841. $sq_name = $sq['varName'] . '.NAOK';
  1842. }
  1843. break;
  1844. default:
  1845. break;
  1846. }
  1847. if (!is_null($sq_name)) {
  1848. $sq_names[] = $sq_name;
  1849. }
  1850. }
  1851. if (count($sq_names) > 0) {
  1852. if (!isset($validationEqn[$questionNum]))
  1853. {
  1854. $validationEqn[$questionNum] = array();
  1855. }
  1856. $sumEqn = 'sum(' . implode(', ', $sq_names) . ')';
  1857. $precision = LEM_DEFAULT_PRECISION;
  1858. if (!is_null($precision))
  1859. {
  1860. $sumEqn = 'round(' . $sumEqn . ', ' . $precision . ')';
  1861. }
  1862. $noanswer_option = '';
  1863. if ($value_range_allows_missing)
  1864. {
  1865. $noanswer_option = ' || count(' . implode(', ', $sq_names) . ') == 0';
  1866. }
  1867. $validationEqn[$questionNum][] = array(
  1868. 'qtype' => $type,
  1869. 'type' => 'max_num_value',
  1870. 'class' => 'sum_range',
  1871. 'eqn' => '(sum(' . implode(', ', $sq_names) . ') <= (' . $max_num_value . ')' . $noanswer_option . ')',
  1872. 'qid' => $questionNum,
  1873. 'sumEqn' => $sumEqn,
  1874. );
  1875. }
  1876. }
  1877. }
  1878. else
  1879. {
  1880. $max_num_value='';
  1881. }
  1882. // multiflexible_min
  1883. // Validation:= sqN >= value (which could be an expression).
  1884. if (isset($qattr['multiflexible_min']) && trim($qattr['multiflexible_min']) != '' && $input_boxes=='1')
  1885. {
  1886. $multiflexible_min = $qattr['multiflexible_min'];
  1887. if ($hasSubqs) {
  1888. $subqs = $qinfo['subqs'];
  1889. $sq_names = array();
  1890. $subqValidEqns = array();
  1891. foreach ($subqs as $sq) {
  1892. $sq_name = NULL;
  1893. switch ($type)
  1894. {
  1895. case ':': //MULTIPLE NUMERICAL QUESTION
  1896. if ($this->sgqaNaming)
  1897. {
  1898. $sgqa = substr($sq['jsVarName'],4);
  1899. $sq_name = '(is_empty(' . $sgqa . '.NAOK) || ' . $sgqa . '.NAOK >= (' . $multiflexible_min . '))';
  1900. }
  1901. else
  1902. {
  1903. $sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || ' . $sq['varName'] . '.NAOK >= (' . $multiflexible_min . '))';
  1904. }
  1905. $subqValidSelector = $sq['jsVarName_on'];
  1906. break;
  1907. default:
  1908. break;
  1909. }
  1910. if (!is_null($sq_name)) {
  1911. $sq_names[] = $sq_name;
  1912. $subqValidEqns[$subqValidSelector] = array(
  1913. 'subqValidEqn' => $sq_name,
  1914. 'subqValidSelector' => $subqValidSelector,
  1915. );
  1916. }
  1917. }
  1918. if (count($sq_names) > 0) {
  1919. if (!isset($validationEqn[$questionNum]))
  1920. {
  1921. $validationEqn[$questionNum] = array();
  1922. }
  1923. $validationEqn[$questionNum][] = array(
  1924. 'qtype' => $type,
  1925. 'type' => 'multiflexible_min',
  1926. 'class' => 'value_range',
  1927. 'eqn' => implode(' && ', $sq_names),
  1928. 'qid' => $questionNum,
  1929. 'subqValidEqns' => $subqValidEqns,
  1930. );
  1931. }
  1932. }
  1933. }
  1934. else
  1935. {
  1936. $multiflexible_min='';
  1937. }
  1938. // multiflexible_max
  1939. // Validation:= sqN <= value (which could be an expression).
  1940. if (isset($qattr['multiflexible_max']) && trim($qattr['multiflexible_max']) != '' && $input_boxes=='1')
  1941. {
  1942. $multiflexible_max = $qattr['multiflexible_max'];
  1943. if ($hasSubqs) {
  1944. $subqs = $qinfo['subqs'];
  1945. $sq_names = array();
  1946. $subqValidEqns = array();
  1947. foreach ($subqs as $sq) {
  1948. $sq_name = NULL;
  1949. switch ($type)
  1950. {
  1951. case ':': //MULTIPLE NUMERICAL QUESTION
  1952. if ($this->sgqaNaming)
  1953. {
  1954. $sgqa = substr($sq['jsVarName'],4);
  1955. $sq_name = '(is_empty(' . $sgqa . '.NAOK) || ' . $sgqa . '.NAOK <= (' . $multiflexible_max . '))';
  1956. }
  1957. else
  1958. {
  1959. $sq_name = '(is_empty(' . $sq['varName'] . '.NAOK) || ' . $sq['varName'] . '.NAOK <= (' . $multiflexible_max . '))';
  1960. }
  1961. $subqValidSelector = $sq['jsVarName_on'];
  1962. break;
  1963. default:
  1964. break;
  1965. }
  1966. if (!is_null($sq_name)) {
  1967. $sq_names[] = $sq_name;
  1968. $subqValidEqns[$subqValidSelector] = array(
  1969. 'subqValidEqn' => $sq_name,
  1970. 'subqValidSelector' => $subqValidSelector,
  1971. );
  1972. }
  1973. }
  1974. if (count($sq_names) > 0) {
  1975. if (!isset($validationEqn[$questionNum]))
  1976. {
  1977. $validationEqn[$questionNum] = array();
  1978. }
  1979. $validationEqn[$questionNum][] = array(
  1980. 'qtype' => $type,
  1981. 'type' => 'multiflexible_max',
  1982. 'class' => 'value_range',
  1983. 'eqn' => implode(' && ', $sq_names),
  1984. 'qid' => $questionNum,
  1985. 'subqValidEqns' => $subqValidEqns,
  1986. );
  1987. }
  1988. }
  1989. }
  1990. else
  1991. {
  1992. $multiflexible_max='';
  1993. }
  1994. // min_num_of_files
  1995. // Validation:= sq_filecount >= value (which could be an expression).
  1996. if (isset($qattr['min_num_of_files']) && trim($qattr['min_num_of_files']) != '')
  1997. {
  1998. $min_num_of_files = $qattr['min_num_of_files'];
  1999. $eqn='';
  2000. $sgqa = $qinfo['sgqa'];
  2001. switch ($type)
  2002. {
  2003. case '|': //List - dropdown
  2004. $eqn = "(" . $sgqa . "_filecount >= (" . $min_num_of_files . "))";
  2005. break;
  2006. default:
  2007. break;
  2008. }
  2009. if ($eqn != '')
  2010. {
  2011. if (!isset($validationEqn[$questionNum]))
  2012. {
  2013. $validationEqn[$questionNum] = array();
  2014. }
  2015. $validationEqn[$questionNum][] = array(
  2016. 'qtype' => $type,
  2017. 'type' => 'min_num_of_files',
  2018. 'class' => 'num_answers',
  2019. 'eqn' => $eqn,
  2020. 'qid' => $questionNum,
  2021. );
  2022. }
  2023. }
  2024. else
  2025. {
  2026. $min_num_of_files = '';
  2027. }
  2028. // max_num_of_files
  2029. // Validation:= sq_filecount <= value (which could be an expression).
  2030. if (isset($qattr['max_num_of_files']) && trim($qattr['max_num_of_files']) != '')
  2031. {
  2032. $max_num_of_files = $qattr['max_num_of_files'];
  2033. $eqn='';
  2034. $sgqa = $qinfo['sgqa'];
  2035. switch ($type)
  2036. {
  2037. case '|': //List - dropdown
  2038. $eqn = "(" . $sgqa . "_filecount <= (" . $max_num_of_files . "))";
  2039. break;
  2040. default:
  2041. break;
  2042. }
  2043. if ($eqn != '')
  2044. {
  2045. if (!isset($validationEqn[$questionNum]))
  2046. {
  2047. $validationEqn[$questionNum] = array();
  2048. }
  2049. $validationEqn[$questionNum][] = array(
  2050. 'qtype' => $type,
  2051. 'type' => 'max_num_of_files',
  2052. 'class' => 'num_answers',
  2053. 'eqn' => $eqn,
  2054. 'qid' => $questionNum,
  2055. );
  2056. }
  2057. }
  2058. else
  2059. {
  2060. $max_num_of_files = '';
  2061. }
  2062. // other_comment_mandatory
  2063. // Validation:= sqN <= value (which could be an expression).
  2064. if (isset($qattr['other_comment_mandatory']) && trim($qattr['other_comment_mandatory']) == '1')
  2065. {
  2066. $other_comment_mandatory = $qattr['other_comment_mandatory'];
  2067. $eqn='';
  2068. if ($other_comment_mandatory == '1' && $this->questionSeq2relevance[$qinfo['qseq']]['other'] == 'Y')
  2069. {
  2070. $sgqa = $qinfo['sgqa'];
  2071. switch ($type)
  2072. {
  2073. case '!': //List - dropdown
  2074. case 'L': //LIST drop-down/radio-button list
  2075. $eqn = "(" . $sgqa . ".NAOK!='-oth-' || (" . $sgqa . ".NAOK=='-oth-' && !is_empty(trim(" . $sgqa . "other.NAOK))))";
  2076. break;
  2077. case 'P': //Multiple choice with comments checkbox + text
  2078. $eqn = "(is_empty(trim(" . $sgqa . "other.NAOK)) || (!is_empty(trim(" . $sgqa . "other.NAOK)) && !is_empty(trim(" . $sgqa . "othercomment.NAOK))))";
  2079. break;
  2080. default:
  2081. break;
  2082. }
  2083. }
  2084. if ($eqn != '')
  2085. {
  2086. if (!isset($validationEqn[$questionNum]))
  2087. {
  2088. $validationEqn[$questionNum] = array();
  2089. }
  2090. $validationEqn[$questionNum][] = array(
  2091. 'qtype' => $type,
  2092. 'type' => 'other_comment_mandatory',
  2093. 'class' => 'other_comment_mandatory',
  2094. 'eqn' => $eqn,
  2095. 'qid' => $questionNum,
  2096. );
  2097. }
  2098. }
  2099. else
  2100. {
  2101. $other_comment_mandatory = '';
  2102. }
  2103. // show_totals
  2104. // TODO - create equations for these?
  2105. // assessment_value
  2106. // TODO? How does it work?
  2107. // The assessment value (referenced how?) = count(sq1,...,sqN) * assessment_value
  2108. // Since there are easy work-arounds to this, skipping it for now
  2109. // preg - a PHP Regular Expression to validate text input fields
  2110. if (isset($qinfo['preg']) && !is_null($qinfo['preg']))
  2111. {
  2112. $preg = $qinfo['preg'];
  2113. if ($hasSubqs) {
  2114. $subqs = $qinfo['subqs'];
  2115. $sq_names = array();
  2116. $subqValidEqns = array();
  2117. foreach ($subqs as $sq) {
  2118. $sq_name = NULL;
  2119. $sgqa = substr($sq['jsVarName'],4);
  2120. switch ($type)
  2121. {
  2122. case 'N': //NUMERICAL QUESTION TYPE
  2123. case 'K': //MULTIPLE NUMERICAL QUESTION
  2124. case 'Q': //MULTIPLE SHORT TEXT
  2125. case ';': //ARRAY (Multi Flexi) Text
  2126. case ':': //ARRAY (Multi Flexi) 1 to 10
  2127. case 'S': //SHORT FREE TEXT
  2128. case 'T': //LONG FREE TEXT
  2129. case 'U': //HUGE FREE TEXT
  2130. if ($this->sgqaNaming)
  2131. {
  2132. $sq_name = '(if(is_empty('.$sgqa.'.NAOK),0,!regexMatch("' . $preg . '", ' . $sgqa . '.NAOK)))';
  2133. }
  2134. else
  2135. {
  2136. $sq_name = '(if(is_empty('.$sq['varName'].'.NAOK),0,!regexMatch("' . $preg . '", ' . $sq['varName'] . '.NAOK)))';
  2137. }
  2138. break;
  2139. default:
  2140. break;
  2141. }
  2142. switch ($type)
  2143. {
  2144. case 'K': //MULTIPLE NUMERICAL QUESTION
  2145. case 'Q': //MULTIPLE SHORT TEXT
  2146. case ';': //ARRAY (Multi Flexi) Text
  2147. case ':': //ARRAY (Multi Flexi) 1 to 10
  2148. if ($this->sgqaNaming)
  2149. {
  2150. $subqValidEqn = '(is_empty('.$sgqa.'.NAOK) || regexMatch("' . $preg . '", ' . $sgqa . '.NAOK))';
  2151. }
  2152. else
  2153. {
  2154. $subqValidEqn = '(is_empty('.$sq['varName'].'.NAOK) || regexMatch("' . $preg . '", ' . $sq['varName'] . '.NAOK))';
  2155. }
  2156. $subqValidSelector = $sq['jsVarName_on'];
  2157. break;
  2158. case 'N': //NUMERICAL QUESTION TYPE
  2159. case 'S': //SHORT FREE TEXT
  2160. case 'T': //LONG FREE TEXT
  2161. case 'U': //HUGE FREE TEXT
  2162. // $subqValidEqn = '(strlen('.$sq['varName'].'.NAOK)==0 || regexMatch("' . $preg . '", ' . $sq['varName'] . '.NAOK))';
  2163. // $subqValidSelector = 'question' . $questionNum . ' :input';
  2164. break;
  2165. default:
  2166. break;
  2167. }
  2168. if (!is_null($sq_name)) {
  2169. $sq_names[] = $sq_name;
  2170. if (isset($subqValidSelector)) {
  2171. $subqValidEqns[$subqValidSelector] = array(
  2172. 'subqValidEqn' => $subqValidEqn,
  2173. 'subqValidSelector' => $subqValidSelector,
  2174. );
  2175. }
  2176. }
  2177. }
  2178. if (count($sq_names) > 0) {
  2179. if (!isset($validationEqn[$questionNum]))
  2180. {
  2181. $validationEqn[$questionNum] = array();
  2182. }
  2183. $validationEqn[$questionNum][] = array(
  2184. 'qtype' => $type,
  2185. 'type' => 'preg',
  2186. 'class' => 'regex_validation',
  2187. 'eqn' => '(sum(' . implode(', ', $sq_names) . ') == 0)',
  2188. 'qid' => $questionNum,
  2189. 'subqValidEqns' => $subqValidEqns,
  2190. );
  2191. }
  2192. }
  2193. }
  2194. else
  2195. {
  2196. $preg='';
  2197. }
  2198. // em_validation_q_tip - a description of the EM validation equation that must be satisfied for the whole question.
  2199. if (isset($qattr['em_validation_q_tip']) && !is_null($qattr['em_validation_q_tip']) && trim($qattr['em_validation_q_tip']) != '')
  2200. {
  2201. $em_validation_q_tip = trim($qattr['em_validation_q_tip']);
  2202. }
  2203. else
  2204. {
  2205. $em_validation_q_tip = '';
  2206. }
  2207. // em_validation_q - an EM validation equation that must be satisfied for the whole question. Uses 'this' in the equation
  2208. if (isset($qattr['em_validation_q']) && !is_null($qattr['em_validation_q']) && trim($qattr['em_validation_q']) != '')
  2209. {
  2210. $em_validation_q = $qattr['em_validation_q'];
  2211. if ($hasSubqs) {
  2212. $subqs = $qinfo['subqs'];
  2213. $sq_names = array();
  2214. foreach ($subqs as $sq) {
  2215. $sq_name = NULL;
  2216. switch ($type)
  2217. {
  2218. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  2219. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  2220. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  2221. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  2222. case 'F': //ARRAY (Flexible) - Row Format
  2223. case 'K': //MULTIPLE NUMERICAL QUESTION
  2224. case 'Q': //MULTIPLE SHORT TEXT
  2225. case ';': //ARRAY (Multi Flexi) Text
  2226. case ':': //ARRAY (Multi Flexi) 1 to 10
  2227. case 'M': //Multiple choice checkbox
  2228. case 'N': //NUMERICAL QUESTION TYPE
  2229. case 'P': //Multiple choice with comments checkbox + text
  2230. case 'R': //RANKING STYLE
  2231. case 'S': //SHORT FREE TEXT
  2232. case 'T': //LONG FREE TEXT
  2233. case 'U': //HUGE FREE TEXT
  2234. if ($this->sgqaNaming)
  2235. {
  2236. $sq_name = '!(' . preg_replace('/\bthis\b/',substr($sq['jsVarName'],4), $em_validation_q) . ')';
  2237. }
  2238. else
  2239. {
  2240. $sq_name = '!(' . preg_replace('/\bthis\b/',$sq['varName'], $em_validation_q) . ')';
  2241. }
  2242. break;
  2243. default:
  2244. break;
  2245. }
  2246. if (!is_null($sq_name)) {
  2247. $sq_names[] = $sq_name;
  2248. }
  2249. }
  2250. if (count($sq_names) > 0) {
  2251. if (!isset($validationEqn[$questionNum]))
  2252. {
  2253. $validationEqn[$questionNum] = array();
  2254. }
  2255. $validationEqn[$questionNum][] = array(
  2256. 'qtype' => $type,
  2257. 'type' => 'em_validation_q',
  2258. 'class' => 'q_fn_validation',
  2259. 'eqn' => '(sum(' . implode(', ', array_unique($sq_names)) . ') == 0)',
  2260. 'qid' => $questionNum,
  2261. );
  2262. }
  2263. }
  2264. }
  2265. else
  2266. {
  2267. $em_validation_q='';
  2268. }
  2269. // em_validation_sq_tip - a description of the EM validation equation that must be satisfied for each subquestion.
  2270. if (isset($qattr['em_validation_sq_tip']) && !is_null($qattr['em_validation_sq_tip']) && trim($qattr['em_validation_sq']) != '')
  2271. {
  2272. $em_validation_sq_tip = trim($qattr['em_validation_sq_tip']);
  2273. }
  2274. else
  2275. {
  2276. $em_validation_sq_tip = '';
  2277. }
  2278. // em_validation_sq - an EM validation equation that must be satisfied for each subquestion. Uses 'this' in the equation
  2279. if (isset($qattr['em_validation_sq']) && !is_null($qattr['em_validation_sq']) && trim($qattr['em_validation_sq']) != '')
  2280. {
  2281. $em_validation_sq = $qattr['em_validation_sq'];
  2282. if ($hasSubqs) {
  2283. $subqs = $qinfo['subqs'];
  2284. $sq_names = array();
  2285. $subqValidEqns = array();
  2286. foreach ($subqs as $sq) {
  2287. $sq_name = NULL;
  2288. switch ($type)
  2289. {
  2290. case 'K': //MULTIPLE NUMERICAL QUESTION
  2291. case 'Q': //MULTIPLE SHORT TEXT
  2292. case ';': //ARRAY (Multi Flexi) Text
  2293. case ':': //ARRAY (Multi Flexi) 1 to 10
  2294. case 'N': //NUMERICAL QUESTION TYPE
  2295. case 'S': //SHORT FREE TEXT
  2296. case 'T': //LONG FREE TEXT
  2297. case 'U': //HUGE FREE TEXT
  2298. if ($this->sgqaNaming)
  2299. {
  2300. $sq_name = '!(' . preg_replace('/\bthis\b/',substr($sq['jsVarName'],4), $em_validation_sq) . ')';
  2301. }
  2302. else
  2303. {
  2304. $sq_name = '!(' . preg_replace('/\bthis\b/',$sq['varName'], $em_validation_sq) . ')';
  2305. }
  2306. break;
  2307. default:
  2308. break;
  2309. }
  2310. switch ($type)
  2311. {
  2312. case 'K': //MULTIPLE NUMERICAL QUESTION
  2313. case 'Q': //MULTIPLE SHORT TEXT
  2314. case ';': //ARRAY (Multi Flexi) Text
  2315. case ':': //ARRAY (Multi Flexi) 1 to 10
  2316. case 'N': //NUMERICAL QUESTION TYPE
  2317. case 'S': //SHORT FREE TEXT
  2318. case 'T': //LONG FREE TEXT
  2319. case 'U': //HUGE FREE TEXT
  2320. if ($this->sgqaNaming)
  2321. {
  2322. $subqValidEqn = '!(' . preg_replace('/\bthis\b/',substr($sq['jsVarName'],4), $em_validation_sq) . ')';
  2323. }
  2324. else
  2325. {
  2326. $subqValidEqn = '(' . preg_replace('/\bthis\b/',$sq['varName'], $em_validation_sq) . ')';
  2327. }
  2328. $subqValidSelector = $sq['jsVarName_on'];
  2329. break;
  2330. default:
  2331. break;
  2332. }
  2333. if (!is_null($sq_name)) {
  2334. $sq_names[] = $sq_name;
  2335. if (isset($subqValidSelector)) {
  2336. $subqValidEqns[$subqValidSelector] = array(
  2337. 'subqValidEqn' => $subqValidEqn,
  2338. 'subqValidSelector' => $subqValidSelector,
  2339. );
  2340. }
  2341. }
  2342. }
  2343. if (count($sq_names) > 0) {
  2344. if (!isset($validationEqn[$questionNum]))
  2345. {
  2346. $validationEqn[$questionNum] = array();
  2347. }
  2348. $validationEqn[$questionNum][] = array(
  2349. 'qtype' => $type,
  2350. 'type' => 'em_validation_sq',
  2351. 'class' => 'sq_fn_validation',
  2352. 'eqn' => '(sum(' . implode(', ', $sq_names) . ') == 0)',
  2353. 'qid' => $questionNum,
  2354. 'subqValidEqns' => $subqValidEqns,
  2355. );
  2356. }
  2357. }
  2358. }
  2359. else
  2360. {
  2361. $em_validation_sq='';
  2362. }
  2363. ////////////////////////////////////////////
  2364. // COMPOSE USER FRIENDLY MIN/MAX MESSAGES //
  2365. ////////////////////////////////////////////
  2366. // Put these in the order you with them to appear in messages.
  2367. $qtips=array();
  2368. // min/max answers
  2369. if ($min_answers!='' || $max_answers!='')
  2370. {
  2371. $_minA = (($min_answers == '') ? "''" : $min_answers);
  2372. $_maxA = (($max_answers == '') ? "''" : $max_answers );
  2373. $qtips['num_answers']=
  2374. "{if(!is_empty($_minA) && is_empty($_maxA) && ($_minA)!=1,sprintf('".$this->gT("Please select at least %s answers")."',fixnum($_minA)),'')}" .
  2375. "{if(!is_empty($_minA) && is_empty($_maxA) && ($_minA)==1,sprintf('".$this->gT("Please select at least one answer")."',fixnum($_minA)),'')}" .
  2376. "{if(is_empty($_minA) && !is_empty($_maxA) && ($_maxA)!=1,sprintf('".$this->gT("Please select at most %s answers")."',fixnum($_maxA)),'')}" .
  2377. "{if(is_empty($_minA) && !is_empty($_maxA) && ($_maxA)==1,sprintf('".$this->gT("Please select at most one answer")."',fixnum($_maxA)),'')}" .
  2378. "{if(!is_empty($_minA) && !is_empty($_maxA) && ($_minA) == ($_maxA) && ($_minA) == 1,'".$this->gT("Please select one answer")."','')}" .
  2379. "{if(!is_empty($_minA) && !is_empty($_maxA) && ($_minA) == ($_maxA) && ($_minA) != 1,sprintf('".$this->gT("Please select %s answers")."',fixnum($_minA)),'')}" .
  2380. "{if(!is_empty($_minA) && !is_empty($_maxA) && ($_minA) != ($_maxA),sprintf('".$this->gT("Please select between %s and %s answers")."',fixnum($_minA),fixnum($_maxA)),'')}";
  2381. }
  2382. // min/max value for each numeric entry
  2383. if ($min_num_value_n!='' || $max_num_value_n!='')
  2384. {
  2385. $_minV = (($min_num_value_n == '') ? "''" : $min_num_value_n);
  2386. $_maxV = (($max_num_value_n == '') ? "''" : $max_num_value_n);
  2387. $qtips['value_range']=
  2388. "{if(!is_empty($_minV) && is_empty($_maxV), sprintf('".$this->gT("Each answer must be at least %s")."',fixnum($_minV)), '')}" .
  2389. "{if(is_empty($_minV) && !is_empty($_maxV), sprintf('".$this->gT("Each answer must be at most %s")."',fixnum($_maxV)), '')}" .
  2390. "{if(!is_empty($_minV) && ($_minV) == ($_maxV),sprintf('".$this->gT("Each answer must be %s")."', fixnum($_minV)), '')}" .
  2391. "{if(!is_empty($_minV) && !is_empty($_maxV) && ($_minV) != ($_maxV), sprintf('".$this->gT("Each answer must be between %s and %s")."', fixnum($_minV), fixnum($_maxV)), '')}";
  2392. }
  2393. // min/max value for each numeric entry - for multi-flexible question type
  2394. if ($multiflexible_min!='' || $multiflexible_max!='')
  2395. {
  2396. $_minV = (($multiflexible_min == '') ? "''" : $multiflexible_min);
  2397. $_maxV = (($multiflexible_max == '') ? "''" : $multiflexible_max);
  2398. $qtips['value_range']=
  2399. "{if(!is_empty($_minV) && is_empty($_maxV), sprintf('".$this->gT("Each answer must be at least %s")."',fixnum($_minV)), '')}" .
  2400. "{if(is_empty($_minV) && !is_empty($_maxV), sprintf('".$this->gT("Each answer must be at most %s")."',fixnum($_maxV)), '')}" .
  2401. "{if(!is_empty($_minV) && ($_minV) == ($_maxV),sprintf('".$this->gT("Each answer must be %s")."', fixnum($_minV)), '')}" .
  2402. "{if(!is_empty($_minV) && !is_empty($_maxV) && ($_minV) != ($_maxV), sprintf('".$this->gT("Each answer must be between %s and %s")."', fixnum($_minV), fixnum($_maxV)), '')}";
  2403. }
  2404. // min/max sum value
  2405. if ($min_num_value!='' || $max_num_value!='')
  2406. {
  2407. $_minV = (($min_num_value == '') ? "''" : $min_num_value);
  2408. $_maxV = (($max_num_value == '') ? "''" : $max_num_value);
  2409. $qtips['sum_range']=
  2410. "{if(!is_empty($_minV) && is_empty($_maxV), sprintf('".$this->gT("The sum must be at least %s")."',fixnum($_minV)), '')}" .
  2411. "{if(is_empty($_minV) && !is_empty($_maxV), sprintf('".$this->gT("The sum must be at most %s")."',fixnum($_maxV)), '')}" .
  2412. "{if(!is_empty($_minV) && ($_minV) == ($_maxV),sprintf('".$this->gT("The sum must equal %s")."', fixnum($_minV)), '')}" .
  2413. "{if(!is_empty($_minV) && !is_empty($_maxV) && ($_minV) != ($_maxV), sprintf('".$this->gT("The sum must be between %s and %s")."', fixnum($_minV), fixnum($_maxV)), '')}";
  2414. }
  2415. // min/max num files
  2416. if ($min_num_of_files !='' || $max_num_of_files !='')
  2417. {
  2418. $_minA = (($min_num_of_files == '') ? "''" : $min_num_of_files);
  2419. $_maxA = (($max_num_of_files == '') ? "''" : $max_num_of_files );
  2420. // TODO - create em_num_files class so can sepately style num_files vs. num_answers
  2421. $qtips['num_answers']=
  2422. "{if(!is_empty($_minA) && is_empty($_maxA) && ($_minA)!=1,sprintf('".$this->gT("Please upload at least %s files")."',fixnum($_minA)),'')}" .
  2423. "{if(!is_empty($_minA) && is_empty($_maxA) && ($_minA)==1,sprintf('".$this->gT("Please upload at least one file")."',fixnum($_minA)),'')}" .
  2424. "{if(is_empty($_minA) && !is_empty($_maxA) && ($_maxA)!=1,sprintf('".$this->gT("Please upload at most %s files")."',fixnum($_maxA)),'')}" .
  2425. "{if(is_empty($_minA) && !is_empty($_maxA) && ($_maxA)==1,sprintf('".$this->gT("Please upload at most one file")."',fixnum($_maxA)),'')}" .
  2426. "{if(!is_empty($_minA) && !is_empty($_maxA) && ($_minA) == ($_maxA) && ($_minA) == 1,'".$this->gT("Please upload one file")."','')}" .
  2427. "{if(!is_empty($_minA) && !is_empty($_maxA) && ($_minA) == ($_maxA) && ($_minA) != 1,sprintf('".$this->gT("Please upload %s files")."',fixnum($_minA)),'')}" .
  2428. "{if(!is_empty($_minA) && !is_empty($_maxA) && ($_minA) != ($_maxA),sprintf('".$this->gT("Please upload between %s and %s files")."',fixnum($_minA),fixnum($_maxA)),'')}";
  2429. }
  2430. // equals_num_value
  2431. if ($equals_num_value!='')
  2432. {
  2433. $qtips['sum_range']=sprintf($this->gT("The sum must equal %s."),'{fixnum('.$equals_num_value.')}');
  2434. }
  2435. // other comment mandatory
  2436. if ($other_comment_mandatory!='')
  2437. {
  2438. if (isset($qattr['other_replace_text']) && trim($qattr['other_replace_text']) != '') {
  2439. $othertext = trim($qattr['other_replace_text']);
  2440. }
  2441. else {
  2442. $othertext = $this->gT('Other:');
  2443. }
  2444. $qtips['other_comment_mandatory']=sprintf($this->gT("If you choose '%s' please also specify your choice in the accompanying text field."),$othertext);
  2445. }
  2446. // regular expression validation
  2447. if ($preg!='')
  2448. {
  2449. // do string replacement here so that curly braces within the regular expression don't trigger an EM error
  2450. // $qtips['regex_validation']=sprintf($this->gT('Each answer must conform to this regular expression: %s'), str_replace(array('{','}'),array('{ ',' }'), $preg));
  2451. $qtips['regex_validation']=$this->gT('Please check the format of your answer.');
  2452. }
  2453. if ($em_validation_sq!='')
  2454. {
  2455. if ($em_validation_sq_tip =='')
  2456. {
  2457. // $stringToParse = htmlspecialchars_decode($em_validation_sq,ENT_QUOTES);
  2458. // $gseq = $this->questionId2groupSeq[$qinfo['qid']];
  2459. // $result = $this->em->ProcessBooleanExpression($stringToParse,$gseq, $qinfo['qseq']);
  2460. // $_validation_tip = $this->em->GetPrettyPrintString();
  2461. // $qtips['sq_fn_validation']=sprintf($this->gT('Each answer must conform to this expression: %s'),$_validation_tip);
  2462. }
  2463. else
  2464. {
  2465. $qtips['sq_fn_validation']=$em_validation_sq_tip;
  2466. }
  2467. }
  2468. // em_validation_q - whole-question validation equation
  2469. if ($em_validation_q!='')
  2470. {
  2471. if ($em_validation_q_tip =='')
  2472. {
  2473. // $stringToParse = htmlspecialchars_decode($em_validation_q,ENT_QUOTES);
  2474. // $gseq = $this->questionId2groupSeq[$qinfo['qid']];
  2475. // $result = $this->em->ProcessBooleanExpression($stringToParse,$gseq, $qinfo['qseq']);
  2476. // $_validation_tip = $this->em->GetPrettyPrintString();
  2477. // $qtips['q_fn_validation']=sprintf($this->gT('The question must conform to this expression: %s'), $_validation_tip);
  2478. }
  2479. else
  2480. {
  2481. $qtips['q_fn_validation']=$em_validation_q_tip;
  2482. }
  2483. }
  2484. if (count($qtips) > 0)
  2485. {
  2486. $validationTips[$questionNum] = $qtips;
  2487. }
  2488. }
  2489. // Consolidate logic across array filters
  2490. $rowdivids = array();
  2491. $order=0;
  2492. foreach ($subQrels as $sq)
  2493. {
  2494. $oldeqn = (isset($rowdivids[$sq['rowdivid']]['eqns']) ? $rowdivids[$sq['rowdivid']]['eqns'] : array());
  2495. $oldtype = (isset($rowdivids[$sq['rowdivid']]['type']) ? $rowdivids[$sq['rowdivid']]['type'] : '');
  2496. $neweqn = (($sq['type'] == 'exclude_all_others') ? array() : array($sq['eqn']));
  2497. $oldeo = (isset($rowdivids[$sq['rowdivid']]['exclusive_options']) ? $rowdivids[$sq['rowdivid']]['exclusive_options'] : array());
  2498. $neweo = (($sq['type'] == 'exclude_all_others') ? array($sq['eqn']) : array());
  2499. $rowdivids[$sq['rowdivid']] = array(
  2500. 'order'=>$order++,
  2501. 'qid'=>$sq['qid'],
  2502. 'rowdivid'=>$sq['rowdivid'],
  2503. 'type'=>$sq['type'] . ';' . $oldtype,
  2504. 'qtype'=>$sq['qtype'],
  2505. 'sgqa'=>$sq['sgqa'],
  2506. 'eqns'=>array_merge($oldeqn, $neweqn),
  2507. 'exclusive_options'=>array_merge($oldeo, $neweo),
  2508. );
  2509. }
  2510. foreach ($rowdivids as $sq)
  2511. {
  2512. $sq['eqn'] = implode(' and ', array_unique(array_merge($sq['eqns'],$sq['exclusive_options']))); // without array_unique, get duplicate of filters for question types 1, :, and ;
  2513. $eos = array_unique($sq['exclusive_options']);
  2514. $isExclusive = '';
  2515. $irrelevantAndExclusive = '';
  2516. if (count($eos) > 0)
  2517. {
  2518. $isExclusive = '!(' . implode(' and ', $eos) . ')';
  2519. $noneos = array_unique($sq['eqns']);
  2520. if (count($noneos) > 0)
  2521. {
  2522. $irrelevantAndExclusive = '(' . implode(' and ', $noneos) . ') and ' . $isExclusive;
  2523. }
  2524. }
  2525. $this->_ProcessSubQRelevance($sq['eqn'], $sq['qid'], $sq['rowdivid'], $sq['type'], $sq['qtype'], $sq['sgqa'], $isExclusive, $irrelevantAndExclusive);
  2526. }
  2527. foreach ($validationEqn as $qid=>$eqns)
  2528. {
  2529. $parts = array();
  2530. $tips = (isset($validationTips[$qid]) ? $validationTips[$qid] : array());
  2531. $subqValidEqns = array();
  2532. $sumEqn = '';
  2533. $sumRemainingEqn = '';
  2534. foreach ($eqns as $v) {
  2535. if (!isset($parts[$v['class']]))
  2536. {
  2537. $parts[$v['class']] = array();
  2538. }
  2539. $parts[$v['class']][] = $v['eqn'];
  2540. // even if there are min/max/preg, the count or total will always be the same
  2541. $sumEqn = (isset($v['sumEqn'])) ? $v['sumEqn'] : $sumEqn;
  2542. $sumRemainingEqn = (isset($v['sumRemainingEqn'])) ? $v['sumRemainingEqn'] : $sumRemainingEqn;
  2543. if (isset($v['subqValidEqns'])) {
  2544. $subqValidEqns[] = $v['subqValidEqns'];
  2545. }
  2546. }
  2547. // combine the sub-question level validation equations into a single validation equation per sub-question
  2548. $subqValidComposite = array();
  2549. foreach ($subqValidEqns as $sqs) {
  2550. foreach ($sqs as $sq)
  2551. {
  2552. if (!isset($subqValidComposite[$sq['subqValidSelector']]))
  2553. {
  2554. $subqValidComposite[$sq['subqValidSelector']] = array(
  2555. 'subqValidSelector' => $sq['subqValidSelector'],
  2556. 'subqValidEqns' => array(),
  2557. );
  2558. }
  2559. $subqValidComposite[$sq['subqValidSelector']]['subqValidEqns'][] = $sq['subqValidEqn'];
  2560. }
  2561. }
  2562. $csubqValidEqns = array();
  2563. foreach ($subqValidComposite as $csq)
  2564. {
  2565. $csubqValidEqns[$csq['subqValidSelector']] = array(
  2566. 'subqValidSelector' => $csq['subqValidSelector'],
  2567. 'subqValidEqn' => implode(' && ', $csq['subqValidEqns']),
  2568. );
  2569. }
  2570. // now combine all classes of validation equations
  2571. $veqns = array();
  2572. foreach ($parts as $vclass=>$eqns)
  2573. {
  2574. $veqns[$vclass] = '(' . implode(' and ', $eqns) . ')';
  2575. }
  2576. $this->qid2validationEqn[$qid] = array(
  2577. 'eqn' => $veqns,
  2578. 'tips' => $tips,
  2579. 'subqValidEqns' => $csubqValidEqns,
  2580. 'sumEqn' => $sumEqn,
  2581. 'sumRemainingEqn' => $sumRemainingEqn,
  2582. );
  2583. }
  2584. // $this->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  2585. }
  2586. /**
  2587. * Recursively find all questions that logically preceded the current array_filter or array_filter_exclude request
  2588. * Note, must support:
  2589. * (a) semicolon-separated list of $qroot codes for either array_filter or array_filter_exclude
  2590. * (b) mixed history of array_filter and array_filter_exclude values
  2591. * @param type $qroot - the question root variable name
  2592. * @param type $aflist - the list of array_filter $qroot codes
  2593. * @param type $afelist - the list of array_filter_exclude $qroot codes
  2594. * @return type
  2595. */
  2596. private function _recursivelyFindAntecdentArrayFilters($qroot, $aflist, $afelist)
  2597. {
  2598. if (isset($this->qrootVarName2arrayFilter[$qroot]))
  2599. {
  2600. if (isset($this->qrootVarName2arrayFilter[$qroot]['array_filter']))
  2601. {
  2602. $_afs = explode(';',$this->qrootVarName2arrayFilter[$qroot]['array_filter']);
  2603. foreach ($_afs as $_af)
  2604. {
  2605. if (in_array($_af,$aflist))
  2606. {
  2607. continue;
  2608. }
  2609. $aflist[] = $_af;
  2610. list($aflist, $afelist) = $this->_recursivelyFindAntecdentArrayFilters($_af, $aflist, $afelist);
  2611. }
  2612. }
  2613. if (isset($this->qrootVarName2arrayFilter[$qroot]['array_filter_exclude']))
  2614. {
  2615. $_afes = explode(';',$this->qrootVarName2arrayFilter[$qroot]['array_filter_exclude']);
  2616. foreach ($_afes as $_afe)
  2617. {
  2618. if (in_array($_afe,$afelist))
  2619. {
  2620. continue;
  2621. }
  2622. $afelist[] = $_afe;
  2623. list($aflist, $afelist) = $this->_recursivelyFindAntecdentArrayFilters($_afe, $aflist, $afelist);
  2624. }
  2625. }
  2626. }
  2627. return array($aflist, $afelist);
  2628. }
  2629. /**
  2630. * Create the arrays needed by ExpressionManager to process LimeSurvey strings.
  2631. * The long part of this function should only be called once per page display (e.g. only if $fieldMap changes)
  2632. *
  2633. * @param <integer> $surveyid
  2634. * @param <Boolean> $forceRefresh
  2635. * @param <Boolean> $anonymized
  2636. * @param <Boolean> $allOnOnePage - if true (like for survey_format), uses certain optimizations
  2637. * @return boolean - true if $fieldmap had been re-created, so ExpressionManager variables need to be re-set
  2638. */
  2639. private function setVariableAndTokenMappingsForExpressionManager($surveyid,$forceRefresh=false,$anonymized=false,$allOnOnePage=false)
  2640. {
  2641. if (isset($_SESSION['LEMforceRefresh'])) {
  2642. unset($_SESSION['LEMforceRefresh']);
  2643. $forceRefresh=true;
  2644. }
  2645. else if (!$forceRefresh && isset($this->knownVars)) {
  2646. return false; // means that those variables have been cached and no changes needed
  2647. }
  2648. $now = microtime(true);
  2649. $this->em->SetSurveyMode($this->surveyMode);
  2650. // TODO - do I need to force refresh, or trust that createFieldMap will cache langauges properly?
  2651. $fieldmap=createFieldMap($surveyid,$style='full',$forceRefresh,false,$_SESSION['LEMlang']);
  2652. $this->sid= $surveyid;
  2653. $this->runtimeTimings[] = array(__METHOD__ . '.createFieldMap',(microtime(true) - $now));
  2654. // LimeExpressionManager::ShowStackTrace();
  2655. $now = microtime(true);
  2656. if (!isset($fieldmap)) {
  2657. return false; // implies an error occurred
  2658. }
  2659. $this->knownVars = array(); // mapping of VarName to Value
  2660. $this->qcode2sgqa = array();
  2661. $this->tempVars = array();
  2662. $this->qid2code = array(); // List of codes for each question - needed to know which to NULL if a question is irrelevant
  2663. $this->jsVar2qid = array();
  2664. $this->qcode2sgq = array();
  2665. $this->alias2varName = array();
  2666. $this->varNameAttr = array();
  2667. $this->questionId2questionSeq = array();
  2668. $this->questionId2groupSeq = array();
  2669. $this->questionSeq2relevance = array();
  2670. $this->groupId2groupSeq = array();
  2671. $this->qid2validationEqn = array();
  2672. $this->groupSeqInfo = array();
  2673. $this->gseq2relevanceStatus = array();
  2674. // Since building array of allowable answers, need to know preset values for certain question types
  2675. $presets = array();
  2676. $presets['G'] = array( //GENDER drop-down list
  2677. 'M' => $this->gT("Male"),
  2678. 'F' => $this->gT("Female"),
  2679. );
  2680. $presets['Y'] = array( //YES/NO radio-buttons
  2681. 'Y' => $this->gT("Yes"),
  2682. 'N' => $this->gT("No"),
  2683. );
  2684. $presets['C'] = array( //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  2685. 'Y' => $this->gT("Yes"),
  2686. 'N' => $this->gT("No"),
  2687. 'U' => $this->gT("Uncertain"),
  2688. );
  2689. $presets['E'] = array( //ARRAY (Increase/Same/Decrease) radio-buttons
  2690. 'I' => $this->gT("Increase"),
  2691. 'S' => $this->gT("Same"),
  2692. 'D' => $this->gT("Decrease"),
  2693. );
  2694. $this->gseq2info = $this->getGroupInfoForEM($surveyid,$_SESSION['LEMlang']);
  2695. foreach ($this->gseq2info as $aGroupInfo)
  2696. {
  2697. $this->groupId2groupSeq[$aGroupInfo['gid']] = $aGroupInfo['group_order'];
  2698. }
  2699. $qattr = $this->getQuestionAttributesForEM($surveyid,NULL,$_SESSION['LEMlang']);
  2700. $this->qattr = $qattr;
  2701. $this->runtimeTimings[] = array(__METHOD__ . ' - question_attributes_model->getQuestionAttributesForEM',(microtime(true) - $now));
  2702. $now = microtime(true);
  2703. $this->qans = $this->getAnswerSetsForEM($surveyid,NULL,$_SESSION['LEMlang']);
  2704. $this->runtimeTimings[] = array(__METHOD__ . ' - answers_model->getAnswerSetsForEM',(microtime(true) - $now));
  2705. $now = microtime(true);
  2706. $q2subqInfo = array();
  2707. $this->multiflexiAnswers=array();
  2708. foreach($fieldmap as $fielddata)
  2709. {
  2710. if (!isset($fielddata['fieldname']) || !preg_match('#^\d+X\d+X\d+#',$fielddata['fieldname']))
  2711. {
  2712. continue; // not an SGQA value
  2713. }
  2714. $sgqa = $fielddata['fieldname'];
  2715. $type = $fielddata['type'];
  2716. $mandatory = $fielddata['mandatory'];
  2717. $fieldNameParts = explode('X',$sgqa);
  2718. $groupNum = $fieldNameParts[1];
  2719. $aid = (isset($fielddata['aid']) ? $fielddata['aid'] : '');
  2720. $sqid = (isset($fielddata['sqid']) ? $fielddata['sqid'] : '');
  2721. $questionId = $fieldNameParts[2];
  2722. $questionNum = $fielddata['qid'];
  2723. $relevance = (isset($fielddata['relevance'])) ? $fielddata['relevance'] : 1;
  2724. $grelevance = (isset($fielddata['grelevance'])) ? $fielddata['grelevance'] : 1;
  2725. $hidden = (isset($qattr[$questionNum]['hidden'])) ? ($qattr[$questionNum]['hidden'] == '1') : false;
  2726. $scale_id = (isset($fielddata['scale_id'])) ? $fielddata['scale_id'] : '0';
  2727. $preg = (isset($fielddata['preg'])) ? $fielddata['preg'] : NULL; // a perl regular exrpession validation function
  2728. $defaultValue = (isset($fielddata['defaultvalue']) ? $fielddata['defaultvalue'] : NULL);
  2729. if (trim($preg) == '') {
  2730. $preg = NULL;
  2731. }
  2732. $help = (isset($fielddata['help'])) ? $fielddata['help']: '';
  2733. $other = (isset($fielddata['other'])) ? $fielddata['other'] : '';
  2734. if (isset($this->questionId2groupSeq[$questionNum])) {
  2735. $groupSeq = $this->questionId2groupSeq[$questionNum];
  2736. }
  2737. else {
  2738. $groupSeq = (isset($fielddata['groupSeq'])) ? $fielddata['groupSeq'] : -1;
  2739. $this->questionId2groupSeq[$questionNum] = $groupSeq;
  2740. }
  2741. if (isset($this->questionId2questionSeq[$questionNum])) {
  2742. $questionSeq = $this->questionId2questionSeq[$questionNum];
  2743. }
  2744. else {
  2745. $questionSeq = (isset($fielddata['questionSeq'])) ? $fielddata['questionSeq'] : -1;
  2746. $this->questionId2questionSeq[$questionNum] = $questionSeq;
  2747. }
  2748. if (!isset($this->groupSeqInfo[$groupSeq])) {
  2749. $this->groupSeqInfo[$groupSeq] = array(
  2750. 'qstart' => $questionSeq,
  2751. 'qend' => $questionSeq,
  2752. );
  2753. }
  2754. else {
  2755. $this->groupSeqInfo[$groupSeq]['qend'] = $questionSeq; // with each question, update so know ending value
  2756. }
  2757. // Create list of codes associated with each question
  2758. $codeList = (isset($this->qid2code[$questionNum]) ? $this->qid2code[$questionNum] : '');
  2759. if ($codeList == '')
  2760. {
  2761. $codeList = $sgqa;
  2762. }
  2763. else
  2764. {
  2765. $codeList .= '|' . $sgqa;
  2766. }
  2767. $this->qid2code[$questionNum] = $codeList;
  2768. $readWrite = 'Y';
  2769. // Set $ansArray
  2770. switch($type)
  2771. {
  2772. case '!': //List - dropdown
  2773. case 'L': //LIST drop-down/radio-button list
  2774. case 'O': //LIST WITH COMMENT drop-down/radio-button list + textarea
  2775. case '1': //Array (Flexible Labels) dual scale // need scale
  2776. case 'H': //ARRAY (Flexible) - Column Format
  2777. case 'F': //ARRAY (Flexible) - Row Format
  2778. case 'R': //RANKING STYLE
  2779. $ansArray = (isset($this->qans[$questionNum]) ? $this->qans[$questionNum] : NULL);
  2780. if ($other == 'Y' && ($type == 'L' || $type == '!'))
  2781. {
  2782. if (preg_match('/other$/',$sgqa))
  2783. {
  2784. $ansArray = NULL; // since the other variable doesn't need it
  2785. }
  2786. else
  2787. {
  2788. $_qattr = isset($qattr[$questionNum]) ? $qattr[$questionNum] : array();
  2789. if (isset($_qattr['other_replace_text']) && trim($_qattr['other_replace_text']) != '') {
  2790. $othertext = trim($_qattr['other_replace_text']);
  2791. }
  2792. else {
  2793. $othertext = $this->gT('Other:');
  2794. }
  2795. $ansArray['0~-oth-'] = '0|' . $othertext;
  2796. }
  2797. }
  2798. break;
  2799. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  2800. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  2801. case ':': //ARRAY (Multi Flexi) 1 to 10
  2802. case '5': //5 POINT CHOICE radio-buttons
  2803. $ansArray=NULL;
  2804. break;
  2805. case 'N': //NUMERICAL QUESTION TYPE
  2806. case 'K': //MULTIPLE NUMERICAL QUESTION
  2807. case 'Q': //MULTIPLE SHORT TEXT
  2808. case ';': //ARRAY (Multi Flexi) Text
  2809. case 'S': //SHORT FREE TEXT
  2810. case 'T': //LONG FREE TEXT
  2811. case 'U': //HUGE FREE TEXT
  2812. case 'M': //Multiple choice checkbox
  2813. case 'P': //Multiple choice with comments checkbox + text
  2814. case 'D': //DATE
  2815. case '*': //Equation
  2816. case 'I': //Language Question
  2817. case '|': //File Upload
  2818. case 'X': //BOILERPLATE QUESTION
  2819. $ansArray = NULL;
  2820. break;
  2821. case 'G': //GENDER drop-down list
  2822. case 'Y': //YES/NO radio-buttons
  2823. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  2824. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  2825. $ansArray = $presets[$type];
  2826. break;
  2827. }
  2828. // set $subqtext text - for display of primary sub-question
  2829. $subqtext = '';
  2830. switch ($type)
  2831. {
  2832. default:
  2833. $subqtext = (isset($fielddata['subquestion']) ? $fielddata['subquestion'] : '');
  2834. break;
  2835. case ':': //ARRAY (Multi Flexi) 1 to 10
  2836. case ';': //ARRAY (Multi Flexi) Text
  2837. $subqtext = (isset($fielddata['subquestion1']) ? $fielddata['subquestion1'] : '');
  2838. $ansList = array();
  2839. if (isset($fielddata['answerList']))
  2840. {
  2841. foreach ($fielddata['answerList'] as $ans) {
  2842. $ansList['1~' . $ans['code']] = $ans['code'] . '|' . $ans['answer'];
  2843. }
  2844. $this->multiflexiAnswers[$questionNum] = $ansList;
  2845. }
  2846. break;
  2847. }
  2848. // Set $varName (question code / questions.title), $rowdivid, $csuffix, $sqsuffix, and $question
  2849. $rowdivid=NULL; // so that blank for types not needing it.
  2850. $sqsuffix='';
  2851. switch($type)
  2852. {
  2853. case '!': //List - dropdown
  2854. case '5': //5 POINT CHOICE radio-buttons
  2855. case 'D': //DATE
  2856. case 'G': //GENDER drop-down list
  2857. case 'I': //Language Question
  2858. case 'L': //LIST drop-down/radio-button list
  2859. case 'N': //NUMERICAL QUESTION TYPE
  2860. case 'O': //LIST WITH COMMENT drop-down/radio-button list + textarea
  2861. case 'S': //SHORT FREE TEXT
  2862. case 'T': //LONG FREE TEXT
  2863. case 'U': //HUGE FREE TEXT
  2864. case 'X': //BOILERPLATE QUESTION
  2865. case 'Y': //YES/NO radio-buttons
  2866. case '|': //File Upload
  2867. case '*': //Equation
  2868. $csuffix = '';
  2869. $sqsuffix = '';
  2870. $varName = $fielddata['title'];
  2871. if ($fielddata['aid'] != '') {
  2872. $varName .= '_' . $fielddata['aid'];
  2873. }
  2874. $question = $fielddata['question'];
  2875. break;
  2876. case '1': //Array (Flexible Labels) dual scale
  2877. $csuffix = $fielddata['aid'] . '#' . $fielddata['scale_id'];
  2878. $sqsuffix = '_' . $fielddata['aid'];
  2879. $varName = $fielddata['title'] . '_' . $fielddata['aid'] . '_' . $fielddata['scale_id'];;
  2880. $question = $fielddata['subquestion'] . '[' . $fielddata['scale'] . ']';
  2881. // $question = $fielddata['question'] . ': ' . $fielddata['subquestion'] . '[' . $fielddata['scale'] . ']';
  2882. $rowdivid = substr($sgqa,0,-2);
  2883. break;
  2884. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  2885. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  2886. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  2887. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  2888. case 'F': //ARRAY (Flexible) - Row Format
  2889. case 'H': //ARRAY (Flexible) - Column Format // note does not have javatbd equivalent - so array filters don't work on it
  2890. case 'K': //MULTIPLE NUMERICAL QUESTION // note does not have javatbd equivalent - so array filters don't work on it, but need rowdivid to process validations
  2891. case 'M': //Multiple choice checkbox
  2892. case 'P': //Multiple choice with comments checkbox + text
  2893. case 'Q': //MULTIPLE SHORT TEXT // note does not have javatbd equivalent - so array filters don't work on it
  2894. case 'R': //RANKING STYLE // note does not have javatbd equivalent - so array filters don't work on it
  2895. $csuffix = $fielddata['aid'];
  2896. $varName = $fielddata['title'] . '_' . $fielddata['aid'];
  2897. $question = $fielddata['subquestion'];
  2898. // $question = $fielddata['question'] . ': ' . $fielddata['subquestion'];
  2899. if ($type != 'H') {
  2900. if ($type == 'P' && preg_match("/comment$/", $sgqa)) {
  2901. // $rowdivid = substr($sgqa,0,-7);
  2902. }
  2903. else {
  2904. $sqsuffix = '_' . $fielddata['aid'];
  2905. $rowdivid = $sgqa;
  2906. }
  2907. }
  2908. break;
  2909. case ':': //ARRAY (Multi Flexi) 1 to 10
  2910. case ';': //ARRAY (Multi Flexi) Text
  2911. $csuffix = $fielddata['aid'];
  2912. $sqsuffix = '_' . substr($fielddata['aid'],0,strpos($fielddata['aid'],'_'));
  2913. $varName = $fielddata['title'] . '_' . $fielddata['aid'];
  2914. $question = $fielddata['subquestion1'] . '[' . $fielddata['subquestion2'] . ']';
  2915. // $question = $fielddata['question'] . ': ' . $fielddata['subquestion1'] . '[' . $fielddata['subquestion2'] . ']';
  2916. $rowdivid = substr($sgqa,0,strpos($sgqa,'_'));
  2917. break;
  2918. }
  2919. // $onlynum
  2920. $onlynum=false; // the default
  2921. switch($type)
  2922. {
  2923. case 'K': //MULTIPLE NUMERICAL QUESTION
  2924. case 'N': //NUMERICAL QUESTION TYPE
  2925. case ':': //ARRAY (Multi Flexi) 1 to 10
  2926. $onlynum=true;
  2927. break;
  2928. case '*': // Equation
  2929. case ';': //ARRAY (Multi Flexi) Text
  2930. case 'Q': //MULTIPLE SHORT TEXT
  2931. case 'S': //SHORT FREE TEXT
  2932. if (isset($qattr[$questionNum]['numbers_only']) && $qattr[$questionNum]['numbers_only']=='1')
  2933. {
  2934. $onlynum=true;
  2935. }
  2936. break;
  2937. case 'L': //LIST drop-down/radio-button list
  2938. case 'M': //Multiple choice checkbox
  2939. case 'P': //Multiple choice with comments checkbox + text
  2940. if (isset($qattr[$questionNum]['other_numbers_only']) && $qattr[$questionNum]['other_numbers_only']=='1' && preg_match('/other$/',$sgqa))
  2941. {
  2942. $onlynum=true;
  2943. }
  2944. break;
  2945. default:
  2946. break;
  2947. }
  2948. // Set $jsVarName_on (for on-page variables - e.g. answerSGQA) and $jsVarName (for off-page variables; the primary name - e.g. javaSGQA)
  2949. switch($type)
  2950. {
  2951. case 'R': //RANKING STYLE
  2952. $jsVarName_on = 'answer' . $sgqa;
  2953. $jsVarName = 'java' . $sgqa;
  2954. break;
  2955. case 'D': //DATE
  2956. case 'N': //NUMERICAL QUESTION TYPE
  2957. case 'S': //SHORT FREE TEXT
  2958. case 'T': //LONG FREE TEXT
  2959. case 'U': //HUGE FREE TEXT
  2960. case 'Q': //MULTIPLE SHORT TEXT
  2961. case 'K': //MULTIPLE NUMERICAL QUESTION
  2962. case 'X': //BOILERPLATE QUESTION
  2963. $jsVarName_on = 'answer' . $sgqa;
  2964. $jsVarName = 'java' . $sgqa;
  2965. break;
  2966. case '!': //List - dropdown
  2967. if (preg_match("/other$/",$sgqa))
  2968. {
  2969. $jsVarName = 'java' . $sgqa;
  2970. $jsVarName_on = 'othertext' . substr($sgqa,0,-5);
  2971. }
  2972. else
  2973. {
  2974. $jsVarName = 'java' . $sgqa;
  2975. $jsVarName_on = $jsVarName;
  2976. }
  2977. break;
  2978. case 'L': //LIST drop-down/radio-button list
  2979. if (preg_match("/other$/",$sgqa))
  2980. {
  2981. $jsVarName = 'java' . $sgqa;
  2982. $jsVarName_on = 'answer' . $sgqa . "text";
  2983. }
  2984. else
  2985. {
  2986. $jsVarName = 'java' . $sgqa;
  2987. $jsVarName_on = $jsVarName;
  2988. }
  2989. break;
  2990. case '5': //5 POINT CHOICE radio-buttons
  2991. case 'G': //GENDER drop-down list
  2992. case 'I': //Language Question
  2993. case 'Y': //YES/NO radio-buttons
  2994. case '*': //Equation
  2995. case '1': //Array (Flexible Labels) dual scale
  2996. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  2997. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  2998. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  2999. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  3000. case 'F': //ARRAY (Flexible) - Row Format
  3001. case 'H': //ARRAY (Flexible) - Column Format
  3002. case 'M': //Multiple choice checkbox
  3003. case 'O': //LIST WITH COMMENT drop-down/radio-button list + textarea
  3004. if ($type == 'O' && preg_match('/_comment$/', $varName))
  3005. {
  3006. $jsVarName_on = 'answer' . $sgqa;
  3007. }
  3008. else
  3009. {
  3010. $jsVarName_on = 'java' . $sgqa;
  3011. }
  3012. $jsVarName = 'java' . $sgqa;
  3013. break;
  3014. case ':': //ARRAY (Multi Flexi) 1 to 10
  3015. case ';': //ARRAY (Multi Flexi) Text
  3016. $jsVarName = 'java' . $sgqa;
  3017. $jsVarName_on = 'answer' . $sgqa;;
  3018. break;
  3019. case '|': //File Upload
  3020. $jsVarName = $sgqa;
  3021. $jsVarName_on = $jsVarName;
  3022. break;
  3023. case 'P': //Multiple choice with comments checkbox + text
  3024. if (preg_match("/(other|comment)$/",$sgqa))
  3025. {
  3026. $jsVarName_on = 'answer' . $sgqa; // is this true for survey.php and not for group.php?
  3027. $jsVarName = 'java' . $sgqa;
  3028. }
  3029. else
  3030. {
  3031. $jsVarName = 'java' . $sgqa;
  3032. $jsVarName_on = $jsVarName;
  3033. }
  3034. break;
  3035. }
  3036. if (!is_null($rowdivid) || $type == 'L' || $type == 'N' || $type == '!' || !is_null($preg)
  3037. || $type == 'S' || $type == 'T' || $type == 'U' || $type == '|') {
  3038. if (!isset($q2subqInfo[$questionNum])) {
  3039. $q2subqInfo[$questionNum] = array(
  3040. 'qid' => $questionNum,
  3041. 'qseq' => $questionSeq,
  3042. 'gseq' => $groupSeq,
  3043. 'sgqa' => $surveyid . 'X' . $groupNum . 'X' . $questionNum,
  3044. 'mandatory'=>$mandatory,
  3045. 'varName' => $varName,
  3046. 'type' => $type,
  3047. 'fieldname' => $sgqa,
  3048. 'preg' => $preg,
  3049. 'rootVarName' => $fielddata['title'],
  3050. );
  3051. }
  3052. if (!isset($q2subqInfo[$questionNum]['subqs'])) {
  3053. $q2subqInfo[$questionNum]['subqs'] = array();
  3054. }
  3055. if ($type == 'L' || $type == '!')
  3056. {
  3057. if (!is_null($ansArray))
  3058. {
  3059. foreach (array_keys($ansArray) as $key)
  3060. {
  3061. $parts = explode('~',$key);
  3062. if ($parts[1] == '-oth-') {
  3063. $parts[1] = 'other';
  3064. }
  3065. $q2subqInfo[$questionNum]['subqs'][] = array(
  3066. 'rowdivid' => $surveyid . 'X' . $groupNum . 'X' . $questionNum . $parts[1],
  3067. 'varName' => $varName,
  3068. 'sqsuffix' => '_' . $parts[1],
  3069. );
  3070. }
  3071. }
  3072. }
  3073. else if ($type == 'N'
  3074. || $type == 'S' || $type == 'T' || $type == 'U') // for $preg
  3075. {
  3076. $q2subqInfo[$questionNum]['subqs'][] = array(
  3077. 'varName' => $varName,
  3078. 'rowdivid' => $surveyid . 'X' . $groupNum . 'X' . $questionNum,
  3079. 'jsVarName' => 'java' . $surveyid . 'X' . $groupNum . 'X' . $questionNum,
  3080. 'jsVarName_on' => $jsVarName_on,
  3081. );
  3082. }
  3083. else
  3084. {
  3085. $q2subqInfo[$questionNum]['subqs'][] = array(
  3086. 'rowdivid' => $rowdivid,
  3087. 'varName' => $varName,
  3088. 'jsVarName_on' => $jsVarName_on,
  3089. 'jsVarName' => $jsVarName,
  3090. 'csuffix' => $csuffix,
  3091. 'sqsuffix' => $sqsuffix,
  3092. );
  3093. }
  3094. }
  3095. $ansList = '';
  3096. if (isset($ansArray) && !is_null($ansArray)) {
  3097. $answers = array();
  3098. foreach ($ansArray as $key => $value) {
  3099. $answers[] = "'" . $key . "':'" . htmlspecialchars(preg_replace('/[[:space:]]/',' ',$value),ENT_QUOTES) . "'";
  3100. }
  3101. $ansList = ",'answers':{ " . implode(",",$answers) . "}";
  3102. }
  3103. // Set mappings of variable names to needed attributes
  3104. $varInfo_Code = array(
  3105. 'jsName_on'=>$jsVarName_on,
  3106. 'jsName'=>$jsVarName,
  3107. 'readWrite'=>$readWrite,
  3108. 'hidden'=>$hidden,
  3109. 'question'=>$question,
  3110. 'qid'=>$questionNum,
  3111. 'gid'=>$groupNum,
  3112. 'grelevance'=>$grelevance,
  3113. 'relevance'=>$relevance,
  3114. 'qcode'=>$varName,
  3115. 'qseq'=>$questionSeq,
  3116. 'gseq'=>$groupSeq,
  3117. 'type'=>$type,
  3118. 'sgqa'=>$sgqa,
  3119. 'ansList'=>$ansList,
  3120. 'ansArray'=>$ansArray,
  3121. 'scale_id'=>$scale_id,
  3122. 'default'=>$defaultValue,
  3123. 'rootVarName'=>$fielddata['title'],
  3124. 'subqtext'=>$subqtext,
  3125. 'rowdivid'=>(is_null($rowdivid) ? '' : $rowdivid),
  3126. 'onlynum'=>$onlynum,
  3127. );
  3128. $this->questionSeq2relevance[$questionSeq] = array(
  3129. 'relevance'=>$relevance,
  3130. 'grelevance'=>$grelevance,
  3131. 'qid'=>$questionNum,
  3132. 'qseq'=>$questionSeq,
  3133. 'gseq'=>$groupSeq,
  3134. 'jsResultVar_on'=>$jsVarName_on,
  3135. 'jsResultVar'=>$jsVarName,
  3136. 'type'=>$type,
  3137. 'hidden'=>$hidden,
  3138. 'gid'=>$groupNum,
  3139. 'mandatory'=>$mandatory,
  3140. 'eqn'=>(($type == '*') ? $question : ''),
  3141. 'help'=>$help,
  3142. 'qtext'=>$fielddata['question'], // $question,
  3143. 'code'=>$varName,
  3144. 'other'=>$other,
  3145. 'default'=>$defaultValue,
  3146. 'rootVarName'=>$fielddata['title'],
  3147. 'rowdivid'=>(is_null($rowdivid) ? '' : $rowdivid),
  3148. 'aid'=>$aid,
  3149. 'sqid'=>$sqid,
  3150. );
  3151. $this->knownVars[$sgqa] = $varInfo_Code;
  3152. $this->qcode2sgqa[$varName]=$sgqa;
  3153. $this->jsVar2qid[$jsVarName] = $questionNum;
  3154. $this->qcode2sgq[$fielddata['title']] = $surveyid . 'X' . $groupNum . 'X' . $questionNum;
  3155. // Create JavaScript arrays
  3156. $this->alias2varName[$varName] = array('jsName'=>$jsVarName, 'jsPart' => "'" . $varName . "':'" . $jsVarName . "'");
  3157. $this->alias2varName[$sgqa] = array('jsName'=>$jsVarName, 'jsPart' => "'" . $sgqa . "':'" . $jsVarName . "'");
  3158. $this->varNameAttr[$jsVarName] = "'" . $jsVarName . "':{ "
  3159. . "'jsName':'" . $jsVarName
  3160. . "','jsName_on':'" . $jsVarName_on
  3161. . "','sgqa':'" . $sgqa
  3162. . "','qid':" . $questionNum
  3163. . ",'gid':" . $groupNum
  3164. // . ",'mandatory':'" . $mandatory
  3165. // . "','question':'" . htmlspecialchars(preg_replace('/[[:space:]]/',' ',$question),ENT_QUOTES)
  3166. . ",'type':'" . $type
  3167. // . "','relevance':'" . (($relevance != '') ? htmlspecialchars(preg_replace('/[[:space:]]/',' ',$relevance),ENT_QUOTES) : 1)
  3168. // . "','readWrite':'" . $readWrite
  3169. // . "','grelevance':'" . (($grelevance != '') ? htmlspecialchars(preg_replace('/[[:space:]]/',' ',$grelevance),ENT_QUOTES) : 1)
  3170. . "','default':'" . (is_null($defaultValue) ? '' : $defaultValue)
  3171. . "','rowdivid':'" . (is_null($rowdivid) ? '' : $rowdivid)
  3172. . "','onlynum':'" . ($onlynum ? '1' : '')
  3173. . "','gseq':" . $groupSeq
  3174. // . ",'qseq':" . $questionSeq
  3175. .$ansList;
  3176. if ($type == 'M' || $type == 'P')
  3177. {
  3178. $this->varNameAttr[$jsVarName] .= ",'question':'" . htmlspecialchars(preg_replace('/[[:space:]]/',' ',$question),ENT_QUOTES) . "'";
  3179. }
  3180. $this->varNameAttr[$jsVarName] .= "}";
  3181. }
  3182. $this->q2subqInfo = $q2subqInfo;
  3183. // Now set tokens
  3184. if (isset($_SESSION[$this->sessid]['token']) && $_SESSION[$this->sessid]['token'] != '')
  3185. {
  3186. //Gather survey data for tokenised surveys, for use in presenting questions
  3187. $_SESSION[$this->sessid]['thistoken']=getTokenData($surveyid, $_SESSION[$this->sessid]['token']);
  3188. $this->knownVars['TOKEN:TOKEN'] = array(
  3189. 'code'=>$_SESSION[$this->sessid]['token'],
  3190. 'jsName_on'=>'',
  3191. 'jsName'=>'',
  3192. 'readWrite'=>'N',
  3193. );
  3194. }
  3195. if (isset($_SESSION[$this->sessid]['thistoken']))
  3196. {
  3197. foreach (array_keys($_SESSION[$this->sessid]['thistoken']) as $tokenkey)
  3198. {
  3199. if ($anonymized)
  3200. {
  3201. $val = "";
  3202. }
  3203. else
  3204. {
  3205. $val = $_SESSION[$this->sessid]['thistoken'][$tokenkey];
  3206. }
  3207. $key = "TOKEN:" . strtoupper($tokenkey);
  3208. $this->knownVars[$key] = array(
  3209. 'code'=>$val,
  3210. 'jsName_on'=>'',
  3211. 'jsName'=>'',
  3212. 'readWrite'=>'N',
  3213. );
  3214. }
  3215. }
  3216. else
  3217. {
  3218. // Read list of available tokens from the tokens table so that preview and error checking works correctly
  3219. $attrs = array_keys(getTokenFieldsAndNames($surveyid));
  3220. $blankVal = array(
  3221. 'code'=>'',
  3222. 'type'=>'',
  3223. 'jsName_on'=>'',
  3224. 'jsName'=>'',
  3225. 'readWrite'=>'N',
  3226. );
  3227. foreach ($attrs as $key)
  3228. {
  3229. if (preg_match('/^(firstname|lastname|email|usesleft|token|attribute_\d+)$/',$key))
  3230. {
  3231. $this->knownVars['TOKEN:' . strtoupper($key)] = $blankVal;
  3232. }
  3233. }
  3234. }
  3235. // set default value for reserved 'this' variable
  3236. $this->knownVars['this'] = array(
  3237. 'jsName_on'=>'',
  3238. 'jsName'=>'',
  3239. 'readWrite'=>'',
  3240. 'hidden'=>'',
  3241. 'question'=>'this',
  3242. 'qid'=>'',
  3243. 'gid'=>'',
  3244. 'grelevance'=>'',
  3245. 'relevance'=>'',
  3246. 'qcode'=>'this',
  3247. 'qseq'=>'',
  3248. 'gseq'=>'',
  3249. 'type'=>'',
  3250. 'sgqa'=>'',
  3251. 'rowdivid'=>'',
  3252. 'ansList'=>'',
  3253. 'ansArray'=>array(),
  3254. 'scale_id'=>'',
  3255. 'default'=>'',
  3256. 'rootVarName'=>'this',
  3257. 'subqtext'=>'',
  3258. 'rowdivid'=>'',
  3259. );
  3260. $this->runtimeTimings[] = array(__METHOD__ . ' - process fieldMap',(microtime(true) - $now));
  3261. usort($this->questionSeq2relevance,'cmpQuestionSeq');
  3262. $this->numQuestions = count($this->questionSeq2relevance);
  3263. $this->numGroups = count($this->groupSeqInfo);
  3264. return true;
  3265. }
  3266. /**
  3267. * Return whether a sub-question is relevant
  3268. * @param <type> $sgqa
  3269. * @return <boolean>
  3270. */
  3271. static function SubQuestionIsRelevant($sgqa)
  3272. {
  3273. $LEM =& LimeExpressionManager::singleton();
  3274. if (!isset($LEM->knownVars[$sgqa]))
  3275. {
  3276. return false;
  3277. }
  3278. $var = $LEM->knownVars[$sgqa];
  3279. $sqrel=1;
  3280. if (isset($var['rowdivid']) && $var['rowdivid'] != '')
  3281. {
  3282. $sqrel = (isset($_SESSION[$LEM->sessid]['relevanceStatus'][$var['rowdivid']]) ? $_SESSION[$LEM->sessid]['relevanceStatus'][$var['rowdivid']] : 1);
  3283. }
  3284. $qid = $var['qid'];
  3285. $qrel = (isset($_SESSION[$LEM->sessid]['relevanceStatus'][$qid]) ? $_SESSION[$LEM->sessid]['relevanceStatus'][$qid] : 1);
  3286. $gseq = $var['gseq'];
  3287. $grel = (isset($_SESSION[$LEM->sessid]['relevanceStatus']['G' . $gseq]) ? $_SESSION[$LEM->sessid]['relevanceStatus']['G' . $gseq] : 1); // group-level relevance based upon grelevance equation
  3288. return ($grel && $qrel && $sqrel);
  3289. }
  3290. /**
  3291. * Return whether question $qid is relevanct
  3292. * @param <type> $qid
  3293. * @return boolean
  3294. */
  3295. static function QuestionIsRelevant($qid)
  3296. {
  3297. $LEM =& LimeExpressionManager::singleton();
  3298. $qrel = (isset($_SESSION[$LEM->sessid]['relevanceStatus'][$qid]) ? $_SESSION[$LEM->sessid]['relevanceStatus'][$qid] : 1);
  3299. $gseq = (isset($LEM->questionId2groupSeq[$qid]) ? $LEM->questionId2groupSeq[$qid] : -1);
  3300. $grel = (isset($_SESSION[$LEM->sessid]['relevanceStatus']['G' . $gseq]) ? $_SESSION[$LEM->sessid]['relevanceStatus']['G' . $gseq] : 1); // group-level relevance based upon grelevance equation
  3301. return ($grel && $qrel);
  3302. }
  3303. /**
  3304. * Returns true if the group is relevant and should be shown
  3305. *
  3306. * @param int $gid
  3307. * @return boolean
  3308. */
  3309. static function GroupIsRelevant($gid)
  3310. {
  3311. $LEM =& LimeExpressionManager::singleton();
  3312. $gseq = $LEM->GetGroupSeq($gid);
  3313. return !$LEM->GroupIsIrrelevantOrHidden($gseq);
  3314. }
  3315. /**
  3316. * Return whether group $gseq is relevant
  3317. * @param <type> $gseq
  3318. * @return boolean
  3319. */
  3320. static function GroupIsIrrelevantOrHidden($gseq)
  3321. {
  3322. $LEM =& LimeExpressionManager::singleton();
  3323. $grel = (isset($_SESSION[$LEM->sessid]['relevanceStatus']['G' . $gseq]) ? $_SESSION[$LEM->sessid]['relevanceStatus']['G' . $gseq] : 1); // group-level relevance based upon grelevance equation
  3324. $gshow = (isset($LEM->indexGseq[$gseq]['show']) ? $LEM->indexGseq[$gseq]['show'] : true); // default to true?
  3325. return !($grel && $gshow);
  3326. }
  3327. /**
  3328. * Check the relevance status of all questions on or before the current group.
  3329. * This generates needed JavaScript for dynamic relevance, and sets flags about which questions and groups are relevant
  3330. */
  3331. function ProcessAllNeededRelevance($onlyThisQseq=NULL)
  3332. {
  3333. // TODO - in a running survey, only need to process the current Group. For Admin mode, do we need to process all prior questions or not?
  3334. // $now = microtime(true);
  3335. $grelComputed=array(); // so only process it once per group
  3336. foreach($this->questionSeq2relevance as $rel)
  3337. {
  3338. if (!is_null($onlyThisQseq) && $onlyThisQseq!=$rel['qseq']) {
  3339. continue;
  3340. }
  3341. $qid = $rel['qid'];
  3342. $gseq = $rel['gseq'];
  3343. if ($this->allOnOnePage) {
  3344. ; // process relevance for all questions
  3345. }
  3346. else if ($gseq != $this->currentGroupSeq) {
  3347. continue;
  3348. }
  3349. $result = $this->_ProcessRelevance(htmlspecialchars_decode($rel['relevance'],ENT_QUOTES),
  3350. $qid,
  3351. $gseq,
  3352. $rel['jsResultVar'],
  3353. $rel['type'],
  3354. $rel['hidden']
  3355. );
  3356. $_SESSION[$this->sessid]['relevanceStatus'][$qid] = $result;
  3357. if (!isset($grelComputed[$gseq])) {
  3358. $this->_ProcessGroupRelevance($gseq);
  3359. $grelComputed[$gseq]=true;
  3360. }
  3361. }
  3362. // $this->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  3363. }
  3364. /**
  3365. * Translate all Expressions, Macros, registered variables, etc. in $string
  3366. * @param <type> $string - the string to be replaced
  3367. * @param <type> $questionNum - the $qid of question being replaced - needed for properly alignment of question-level relevance and tailoring
  3368. * @param <type> $replacementFields - optional replacement values
  3369. * @param <boolean> $debug - if true,write translations for this page to html-formatted log file
  3370. * @param <type> $numRecursionLevels - the number of times to recursively subtitute values in this string
  3371. * @param <type> $whichPrettyPrintIteration - if want to pretty-print the source string, which recursion level should be pretty-printed
  3372. * @param <type> $noReplacements - true if we already know that no replacements are needed (e.g. there are no curly braces)
  3373. * @return <type> - the original $string with all replacements done.
  3374. */
  3375. static function ProcessString($string, $questionNum=NULL, $replacementFields=array(), $debug=false, $numRecursionLevels=1, $whichPrettyPrintIteration=1, $noReplacements=false, $timeit=true, $staticReplacement=false)
  3376. {
  3377. $now = microtime(true);
  3378. $LEM =& LimeExpressionManager::singleton();
  3379. if ($noReplacements) {
  3380. $LEM->em->SetPrettyPrintSource($string);
  3381. return $string;
  3382. }
  3383. if (isset($replacementFields) && is_array($replacementFields) && count($replacementFields) > 0)
  3384. {
  3385. $replaceArray = array();
  3386. foreach ($replacementFields as $key => $value) {
  3387. $replaceArray[$key] = array(
  3388. 'code'=>$value,
  3389. 'jsName_on'=>'',
  3390. 'jsName'=>'',
  3391. 'readWrite'=>'N',
  3392. );
  3393. }
  3394. $LEM->tempVars = $replaceArray;
  3395. }
  3396. $questionSeq = -1;
  3397. $groupSeq = -1;
  3398. if (!is_null($questionNum)) {
  3399. $questionSeq = isset($LEM->questionId2questionSeq[$questionNum]) ? $LEM->questionId2questionSeq[$questionNum] : -1;
  3400. $groupSeq = isset($LEM->questionId2groupSeq[$questionNum]) ? $LEM->questionId2groupSeq[$questionNum] : -1;
  3401. }
  3402. $stringToParse = $string; // decode called later htmlspecialchars_decode($string,ENT_QUOTES);
  3403. $qnum = is_null($questionNum) ? 0 : $questionNum;
  3404. $result = $LEM->em->sProcessStringContainingExpressions($stringToParse,$qnum, $numRecursionLevels, $whichPrettyPrintIteration, $groupSeq, $questionSeq, $staticReplacement);
  3405. if ($timeit) {
  3406. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  3407. }
  3408. return $result;
  3409. }
  3410. /**
  3411. * Compute Relevance, processing $eqn to get a boolean value. If there are syntax errors, return false.
  3412. * @param <type> $eqn - the relevance equation
  3413. * @param <type> $questionNum - needed to align question-level relevance and tailoring
  3414. * @param <type> $jsResultVar - this variable determines whether irrelevant questions are hidden
  3415. * @param <type> $type - question type
  3416. * @param <type> $hidden - whether question should always be hidden
  3417. * @return <type>
  3418. */
  3419. static function ProcessRelevance($eqn,$questionNum=NULL,$jsResultVar=NULL,$type=NULL,$hidden=0)
  3420. {
  3421. $LEM =& LimeExpressionManager::singleton();
  3422. return $LEM->_ProcessRelevance($eqn,$questionNum,NULL,$jsResultVar,$type,$hidden);
  3423. }
  3424. /**
  3425. * Compute Relevance, processing $eqn to get a boolean value. If there are syntax errors, return false.
  3426. * @param <type> $eqn - the relevance equation
  3427. * @param <type> $questionNum - needed to align question-level relevance and tailoring
  3428. * @param <type> $jsResultVar - this variable determines whether irrelevant questions are hidden
  3429. * @param <type> $type - question type
  3430. * @param <type> $hidden - whether question should always be hidden
  3431. * @return <type>
  3432. */
  3433. private function _ProcessRelevance($eqn,$questionNum=NULL,$gseq=NULL,$jsResultVar=NULL,$type=NULL,$hidden=0)
  3434. {
  3435. // These will be called in the order that questions are supposed to be asked
  3436. // TODO - cache results and generated JavaScript equations?
  3437. if (!isset($eqn) || trim($eqn=='') || trim($eqn)=='1')
  3438. {
  3439. $this->groupRelevanceInfo[] = array(
  3440. 'qid' => $questionNum,
  3441. 'gseq' => $gseq,
  3442. 'eqn' => $eqn,
  3443. 'result' => true,
  3444. 'numJsVars' => 0,
  3445. 'relevancejs' => '',
  3446. 'relevanceVars' => '',
  3447. 'jsResultVar'=> $jsResultVar,
  3448. 'type'=>$type,
  3449. 'hidden'=>$hidden,
  3450. 'hasErrors'=>false,
  3451. );
  3452. return true;
  3453. }
  3454. $questionSeq = -1;
  3455. $groupSeq = -1;
  3456. if (!is_null($questionNum)) {
  3457. $questionSeq = isset($this->questionId2questionSeq[$questionNum]) ? $this->questionId2questionSeq[$questionNum] : -1;
  3458. $groupSeq = isset($this->questionId2groupSeq[$questionNum]) ? $this->questionId2groupSeq[$questionNum] : -1;
  3459. }
  3460. $stringToParse = htmlspecialchars_decode($eqn,ENT_QUOTES);
  3461. $result = $this->em->ProcessBooleanExpression($stringToParse,$groupSeq, $questionSeq);
  3462. $hasErrors = $this->em->HasErrors();
  3463. if (!is_null($questionNum) && !is_null($jsResultVar)) { // so if missing either, don't generate JavaScript for this - means off-page relevance.
  3464. $jsVars = $this->em->GetJSVarsUsed();
  3465. $relevanceVars = implode('|',$this->em->GetJSVarsUsed());
  3466. $relevanceJS = $this->em->GetJavaScriptEquivalentOfExpression();
  3467. $this->groupRelevanceInfo[] = array(
  3468. 'qid' => $questionNum,
  3469. 'gseq' => $gseq,
  3470. 'eqn' => $eqn,
  3471. 'result' => $result,
  3472. 'numJsVars' => count($jsVars),
  3473. 'relevancejs' => $relevanceJS,
  3474. 'relevanceVars' => $relevanceVars,
  3475. 'jsResultVar' => $jsResultVar,
  3476. 'type'=>$type,
  3477. 'hidden'=>$hidden,
  3478. 'hasErrors'=>$hasErrors,
  3479. );
  3480. }
  3481. return $result;
  3482. }
  3483. /**
  3484. * Create JavaScript needed to process sub-question-level relevance (e.g. for array_filter and _exclude)
  3485. * @param <type> $eqn - the equation to parse
  3486. * @param <type> $questionNum - the question number - needed to align relavance and tailoring blocks
  3487. * @param <type> $rowdivid - the javascript ID that needs to be shown/hidden in order to control array_filter visibility
  3488. * @param <type> $type - the type of sub-question relevance (e.g. 'array_filter', 'array_filter_exclude')
  3489. * @return <type>
  3490. */
  3491. private function _ProcessSubQRelevance($eqn,$questionNum=NULL,$rowdivid=NULL, $type=NULL, $qtype=NULL, $sgqa=NULL, $isExclusive='', $irrelevantAndExclusive='')
  3492. {
  3493. // These will be called in the order that questions are supposed to be asked
  3494. if (!isset($eqn) || trim($eqn=='') || trim($eqn)=='1')
  3495. {
  3496. return true;
  3497. }
  3498. $questionSeq = -1;
  3499. $groupSeq = -1;
  3500. if (!is_null($questionNum)) {
  3501. $questionSeq = isset($this->questionId2questionSeq[$questionNum]) ? $this->questionId2questionSeq[$questionNum] : -1;
  3502. $groupSeq = isset($this->questionId2groupSeq[$questionNum]) ? $this->questionId2groupSeq[$questionNum] : -1;
  3503. }
  3504. $stringToParse = htmlspecialchars_decode($eqn,ENT_QUOTES);
  3505. $result = $this->em->ProcessBooleanExpression($stringToParse,$groupSeq, $questionSeq);
  3506. $hasErrors = $this->em->HasErrors();
  3507. $prettyPrint = '';
  3508. if (($this->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX) {
  3509. $prettyPrint= $this->em->GetPrettyPrintString();
  3510. }
  3511. if (!is_null($questionNum)) {
  3512. $jsVars = $this->em->GetJSVarsUsed();
  3513. $relevanceVars = implode('|',$this->em->GetJSVarsUsed());
  3514. $relevanceJS = $this->em->GetJavaScriptEquivalentOfExpression();
  3515. $isExclusiveJS='';
  3516. $irrelevantAndExclusiveJS='';
  3517. // Only need to extract JS, since will already have Vars and error counts from main equation
  3518. if ($isExclusive != '')
  3519. {
  3520. $this->em->ProcessBooleanExpression($isExclusive,$groupSeq, $questionSeq);
  3521. $isExclusiveJS = $this->em->GetJavaScriptEquivalentOfExpression();
  3522. }
  3523. if ($irrelevantAndExclusive != '')
  3524. {
  3525. $this->em->ProcessBooleanExpression($irrelevantAndExclusive,$groupSeq, $questionSeq);
  3526. $irrelevantAndExclusiveJS = $this->em->GetJavaScriptEquivalentOfExpression();
  3527. }
  3528. if (!isset($this->subQrelInfo[$questionNum])) {
  3529. $this->subQrelInfo[$questionNum] = array();
  3530. }
  3531. $this->subQrelInfo[$questionNum][$rowdivid] = array(
  3532. 'qid' => $questionNum,
  3533. 'eqn' => $eqn,
  3534. 'prettyPrintEqn' => $prettyPrint,
  3535. 'result' => $result,
  3536. 'numJsVars' => count($jsVars),
  3537. 'relevancejs' => $relevanceJS,
  3538. 'relevanceVars' => $relevanceVars,
  3539. 'rowdivid' => $rowdivid,
  3540. 'type'=>$type,
  3541. 'qtype'=>$qtype,
  3542. 'sgqa'=>$sgqa,
  3543. 'hasErrors'=>$hasErrors,
  3544. 'isExclusiveJS'=>$isExclusiveJS,
  3545. 'irrelevantAndExclusiveJS'=>$irrelevantAndExclusiveJS,
  3546. );
  3547. }
  3548. return $result;
  3549. }
  3550. private function _ProcessGroupRelevance($groupSeq)
  3551. {
  3552. // These will be called in the order that questions are supposed to be asked
  3553. if ($groupSeq == -1) {
  3554. return; // invalid group, so ignore
  3555. }
  3556. $eqn = (isset($this->gseq2info[$groupSeq]['grelevance']) ? $this->gseq2info[$groupSeq]['grelevance'] : 1);
  3557. if (is_null($eqn) || trim($eqn=='') || trim($eqn)=='1')
  3558. {
  3559. $this->gRelInfo[$groupSeq] = array(
  3560. 'gseq' => $groupSeq,
  3561. 'eqn' => '',
  3562. 'result' => 1,
  3563. 'numJsVars' => 0,
  3564. 'relevancejs' => '',
  3565. 'relevanceVars' => '',
  3566. 'prettyprint'=> '',
  3567. );
  3568. $_SESSION[$this->sessid]['relevanceStatus']['G' . $groupSeq] = 1;
  3569. return;
  3570. }
  3571. $stringToParse = htmlspecialchars_decode($eqn,ENT_QUOTES);
  3572. $result = $this->em->ProcessBooleanExpression($stringToParse,$groupSeq);
  3573. $hasErrors = $this->em->HasErrors();
  3574. $jsVars = $this->em->GetJSVarsUsed();
  3575. $relevanceVars = implode('|',$this->em->GetJSVarsUsed());
  3576. $relevanceJS = $this->em->GetJavaScriptEquivalentOfExpression();
  3577. $prettyPrint = $this->em->GetPrettyPrintString();
  3578. $this->gRelInfo[$groupSeq] = array(
  3579. 'gseq' => $groupSeq,
  3580. 'eqn' => $stringToParse,
  3581. 'result' => $result,
  3582. 'numJsVars' => count($jsVars),
  3583. 'relevancejs' => $relevanceJS,
  3584. 'relevanceVars' => $relevanceVars,
  3585. 'prettyprint'=> $prettyPrint,
  3586. 'hasErrors' => $hasErrors,
  3587. );
  3588. $_SESSION[$this->sessid]['relevanceStatus']['G' . $groupSeq] = $result;
  3589. }
  3590. /**
  3591. * Used to show potential syntax errors of processing Relevance or Equations.
  3592. * @return <type>
  3593. */
  3594. static function GetLastPrettyPrintExpression()
  3595. {
  3596. $LEM =& LimeExpressionManager::singleton();
  3597. return $LEM->em->GetLastPrettyPrintExpression();
  3598. }
  3599. /**
  3600. * Expand "self.suffix" and "that.qcode.suffix" into canonical list of variable names
  3601. * @param type $qseq
  3602. * @param type $varname
  3603. */
  3604. static function GetAllVarNamesForQ($qseq,$varname)
  3605. {
  3606. $LEM =& LimeExpressionManager::singleton();
  3607. $parts = explode('.',$varname);
  3608. $qroot = '';
  3609. $suffix = '';
  3610. $sqpatts = array();
  3611. $nosqpatts = array();
  3612. $sqpatt = '';
  3613. $nosqpatt = '';
  3614. $comments = '';
  3615. if ($parts[0] == 'self')
  3616. {
  3617. $type = 'self';
  3618. }
  3619. else
  3620. {
  3621. $type = 'that';
  3622. array_shift($parts);
  3623. if (isset($parts[0]))
  3624. {
  3625. $qroot = $parts[0];
  3626. }
  3627. else
  3628. {
  3629. return $varname;
  3630. }
  3631. }
  3632. array_shift($parts);
  3633. if (count($parts) > 0)
  3634. {
  3635. if (preg_match('/^' . ExpressionManager::$RDP_regex_var_attr . '$/',$parts[count($parts)-1]))
  3636. {
  3637. $suffix = '.' . $parts[count($parts)-1];
  3638. array_pop($parts);
  3639. }
  3640. }
  3641. foreach($parts as $part)
  3642. {
  3643. if ($part == 'nocomments')
  3644. {
  3645. $comments = 'N';
  3646. }
  3647. else if ($part == 'comments')
  3648. {
  3649. $comments = 'Y';
  3650. }
  3651. else if (preg_match('/^sq_.+$/',$part))
  3652. {
  3653. $sqpatts[] = substr($part,3);
  3654. }
  3655. else if (preg_match('/^nosq_.+$/',$part))
  3656. {
  3657. $nosqpatts[] = substr($part,5);
  3658. }
  3659. else
  3660. {
  3661. return $varname; // invalid
  3662. }
  3663. }
  3664. $sqpatt = implode('|',$sqpatts);
  3665. $nosqpatt = implode('|',$nosqpatts);
  3666. $vars = array();
  3667. foreach ($LEM->knownVars as $kv)
  3668. {
  3669. if ($type == 'self')
  3670. {
  3671. if (!isset($kv['qseq']) || $kv['qseq'] != $qseq || trim($kv['sgqa']) == '')
  3672. {
  3673. continue;
  3674. }
  3675. }
  3676. else
  3677. {
  3678. if (!isset($kv['rootVarName']) || $kv['rootVarName'] != $qroot)
  3679. {
  3680. continue;
  3681. }
  3682. }
  3683. if ($comments != '')
  3684. {
  3685. if ($comments == 'Y' && !preg_match('/comment$/',$kv['sgqa']))
  3686. {
  3687. continue;
  3688. }
  3689. if ($comments == 'N' && preg_match('/comment$/',$kv['sgqa']))
  3690. {
  3691. continue;
  3692. }
  3693. }
  3694. $sgq = $LEM->sid . 'X' . $kv['gid'] . 'X' . $kv['qid'];
  3695. $ext = substr($kv['sgqa'],strlen($sgq));
  3696. if ($sqpatt != '')
  3697. {
  3698. if (!preg_match('/'.$sqpatt.'/',$ext))
  3699. {
  3700. continue;
  3701. }
  3702. }
  3703. if ($nosqpatt != '')
  3704. {
  3705. if (preg_match('/'.$nosqpatt.'/',$ext))
  3706. {
  3707. continue;
  3708. }
  3709. }
  3710. $vars[] = $kv['sgqa'] . $suffix;
  3711. }
  3712. if (count($vars) > 0)
  3713. {
  3714. return implode(',',$vars);
  3715. }
  3716. return $varname; // invalid
  3717. }
  3718. /**
  3719. * Should be first function called on each page - sets/clears internally needed variables
  3720. * @param <type> $allOnOnePage - true if StartProcessingGroup will be called multiple times on this page - does some optimizatinos
  3721. * @param <type> $rooturl - if set, this tells LEM to enable hyperlinking of syntax highlighting to ease editing of questions
  3722. * @param <boolean> $initializeVars - if true, initializes the replacement variables to enable syntax highlighting on admin pages
  3723. */
  3724. static function StartProcessingPage($allOnOnePage=false,$initializeVars=false)
  3725. {
  3726. // $now = microtime(true);
  3727. $LEM =& LimeExpressionManager::singleton();
  3728. $LEM->pageRelevanceInfo=array();
  3729. $LEM->pageTailorInfo=array();
  3730. $LEM->allOnOnePage=$allOnOnePage;
  3731. $LEM->processedRelevance=false;
  3732. $LEM->surveyOptions['hyperlinkSyntaxHighlighting']=true; // this will be temporary - should be reset in running survey
  3733. $LEM->qid2exclusiveAuto=array();
  3734. $surveyinfo = (isset($LEM->sid) ? getSurveyInfo($LEM->sid) : null);
  3735. if (isset($surveyinfo['assessments']) && $surveyinfo['assessments']=='Y')
  3736. {
  3737. $LEM->surveyOptions['assessments']=true;
  3738. }
  3739. // $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  3740. $LEM->initialized=true;
  3741. if ($initializeVars)
  3742. {
  3743. $LEM->em->StartProcessingGroup(
  3744. isset($_SESSION['LEMsid']) ? $_SESSION['LEMsid'] : NULL,
  3745. '',
  3746. true
  3747. );
  3748. $LEM->setVariableAndTokenMappingsForExpressionManager($_SESSION['LEMsid']);
  3749. }
  3750. }
  3751. /**
  3752. * Initialize a survey so can use EM to manage navigation
  3753. * @param <type> $surveyid
  3754. * @param <type> $surveyMode
  3755. * @param <type> $anonymized
  3756. * @param <type> $forceRefresh
  3757. */
  3758. static function StartSurvey($surveyid,$surveyMode='group',$options=NULL,$forceRefresh=false,$debugLevel=0)
  3759. {
  3760. $LEM =& LimeExpressionManager::singleton();
  3761. $LEM->sid=sanitize_int($surveyid);
  3762. $LEM->sessid = 'survey_' . $LEM->sid;
  3763. $LEM->em->StartProcessingGroup($surveyid);
  3764. if (is_null($options)) {
  3765. $options = array();
  3766. }
  3767. $LEM->surveyOptions['active'] = (isset($options['active']) ? $options['active'] : false);
  3768. $LEM->surveyOptions['allowsave'] = (isset($options['allowsave']) ? $options['allowsave'] : false);
  3769. $LEM->surveyOptions['anonymized'] = (isset($options['anonymized']) ? $options['anonymized'] : false);
  3770. $LEM->surveyOptions['assessments'] = (isset($options['assessments']) ? $options['assessments'] : false);
  3771. $LEM->surveyOptions['datestamp'] = (isset($options['datestamp']) ? $options['datestamp'] : false);
  3772. $LEM->surveyOptions['deletenonvalues'] = (isset($options['deletenonvalues']) ? ($options['deletenonvalues']=='1') : true);
  3773. $LEM->surveyOptions['hyperlinkSyntaxHighlighting'] = (isset($options['hyperlinkSyntaxHighlighting']) ? $options['hyperlinkSyntaxHighlighting'] : false);
  3774. $LEM->surveyOptions['ipaddr'] = (isset($options['ipaddr']) ? $options['ipaddr'] : false);
  3775. $LEM->surveyOptions['radix'] = (isset($options['radix']) ? $options['radix'] : '.');
  3776. $LEM->surveyOptions['refurl'] = (isset($options['refurl']) ? $options['refurl'] : NULL);
  3777. $LEM->surveyOptions['savetimings'] = (isset($options['savetimings']) ? $options['savetimings'] : '');
  3778. $LEM->sgqaNaming = (isset($options['sgqaNaming']) ? ($options['sgqaNaming']=="Y") : true); // TODO default should eventually be false
  3779. $LEM->surveyOptions['startlanguage'] = (isset($options['startlanguage']) ? $options['startlanguage'] : 'en');
  3780. $LEM->surveyOptions['surveyls_dateformat'] = (isset($options['surveyls_dateformat']) ? $options['surveyls_dateformat'] : 1);
  3781. $LEM->surveyOptions['tablename'] = (isset($options['tablename']) ? $options['tablename'] : '{{survey_' . $LEM->sid . '}}');
  3782. $LEM->surveyOptions['tablename_timings'] = ((isset($options['savetimings']) && $options['savetimings'] == 'Y') ? '{{survey_' . $LEM->sid . '_timings}}' : '');
  3783. $LEM->surveyOptions['target'] = (isset($options['target']) ? $options['target'] : '/temp/files/');
  3784. $LEM->surveyOptions['timeadjust'] = (isset($options['timeadjust']) ? $options['timeadjust'] : 0);
  3785. $LEM->surveyOptions['tempdir'] = (isset($options['tempdir']) ? $options['tempdir'] : '/temp/');
  3786. $LEM->surveyOptions['token'] = (isset($options['token']) ? $options['token'] : NULL);
  3787. $LEM->debugLevel=$debugLevel;
  3788. $_SESSION[$LEM->sessid]['LEMdebugLevel']=$debugLevel; // need acces to SESSSION to decide whether to cache serialized instance of $LEM
  3789. switch ($surveyMode) {
  3790. case 'survey':
  3791. $LEM->allOnOnePage=true;
  3792. $LEM->surveyMode = 'survey';
  3793. break;
  3794. case 'question':
  3795. $LEM->allOnOnePage=false;
  3796. $LEM->surveyMode = 'question';
  3797. break;
  3798. default:
  3799. case 'group':
  3800. $LEM->allOnOnePage=false;
  3801. $LEM->surveyMode = 'group';
  3802. break;
  3803. }
  3804. $LEM->setVariableAndTokenMappingsForExpressionManager($surveyid,$forceRefresh,$LEM->surveyOptions['anonymized'],$LEM->allOnOnePage);
  3805. $LEM->currentGroupSeq=-1;
  3806. $LEM->currentQuestionSeq=-1; // for question-by-question mode
  3807. $LEM->indexGseq=array();
  3808. $LEM->indexQseq=array();
  3809. $LEM->qrootVarName2arrayFilter=array();
  3810. if (isset($_SESSION[$LEM->sessid]['startingValues']) && is_array($_SESSION[$LEM->sessid]['startingValues']) && count($_SESSION[$LEM->sessid]['startingValues']) > 0)
  3811. {
  3812. $startingValues = array();
  3813. foreach ($_SESSION[$LEM->sessid]['startingValues'] as $k=>$value)
  3814. {
  3815. if (isset($LEM->knownVars[$k]))
  3816. {
  3817. $knownVar = $LEM->knownVars[$k];
  3818. }
  3819. else if (isset($LEM->qcode2sgqa[$k]))
  3820. {
  3821. $knownVar = $LEM->knownVars[$LEM->qcode2sgqa[$k]];
  3822. }
  3823. else if (isset($LEM->tempVars[$k]))
  3824. {
  3825. $knownVar = $LEM->tempVar[$k];
  3826. }
  3827. else
  3828. {
  3829. continue;
  3830. }
  3831. if (!isset($knownVar['jsName']))
  3832. {
  3833. continue;
  3834. }
  3835. switch ($knownVar['type'])
  3836. {
  3837. case 'D': //DATE
  3838. if (trim($value)=="")
  3839. {
  3840. $value = NULL;
  3841. }
  3842. else
  3843. {
  3844. $dateformatdatat=getDateFormatData($LEM->surveyOptions['surveyls_dateformat']);
  3845. $datetimeobj = new Date_Time_Converter($value, $dateformatdatat['phpdate']);
  3846. $value=$datetimeobj->convert("Y-m-d");
  3847. }
  3848. break;
  3849. case 'N': //NUMERICAL QUESTION TYPE
  3850. case 'K': //MULTIPLE NUMERICAL QUESTION
  3851. if (trim($value)=="") {
  3852. $value = NULL;
  3853. }
  3854. else {
  3855. $value = sanitize_float($value);
  3856. }
  3857. break;
  3858. case '|': //File Upload
  3859. $value=NULL; // can't upload a file via GET
  3860. break;
  3861. }
  3862. $_SESSION[$LEM->sessid][$knownVar['sgqa']] = $value;
  3863. $LEM->updatedValues[$knownVar['sgqa']]=array(
  3864. 'type'=>$knownVar['type'],
  3865. 'value'=>$value,
  3866. );
  3867. }
  3868. $LEM->_UpdateValuesInDatabase(NULL);
  3869. }
  3870. return array(
  3871. 'hasNext'=>true,
  3872. 'hasPrevious'=>false,
  3873. );
  3874. }
  3875. static function NavigateBackwards()
  3876. {
  3877. $now = microtime(true);
  3878. $LEM =& LimeExpressionManager::singleton();
  3879. $LEM->ParseResultCache=array(); // to avoid running same test more than once for a given group
  3880. $LEM->updatedValues = array();
  3881. switch ($LEM->surveyMode)
  3882. {
  3883. case 'survey':
  3884. // should never be called?
  3885. break;
  3886. case 'group':
  3887. // First validate the current group
  3888. $LEM->StartProcessingPage();
  3889. $updatedValues=$LEM->ProcessCurrentResponses();
  3890. $message = '';
  3891. while (true)
  3892. {
  3893. $LEM->currentQset = array(); // reset active list of questions
  3894. if (--$LEM->currentGroupSeq < 0)
  3895. {
  3896. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  3897. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  3898. $LEM->lastMoveResult = array(
  3899. 'at_start'=>true,
  3900. 'finished'=>false,
  3901. 'message'=>$message,
  3902. 'unansweredSQs'=>(isset($result['unansweredSQs']) ? $result['unansweredSQs'] : ''),
  3903. 'invalidSQs'=>(isset($result['invalidSQs']) ? $result['invalidSQs'] : ''),
  3904. );
  3905. return $LEM->lastMoveResult;
  3906. }
  3907. $result = $LEM->_ValidateGroup($LEM->currentGroupSeq);
  3908. if (is_null($result)) {
  3909. continue; // this is an invalid group - skip it
  3910. }
  3911. $message .= $result['message'];
  3912. if (!$result['relevant'] || $result['hidden'])
  3913. {
  3914. // then skip this group - assume already saved?
  3915. continue;
  3916. }
  3917. else
  3918. {
  3919. // display new group
  3920. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  3921. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  3922. $LEM->lastMoveResult = array(
  3923. 'at_start'=>false,
  3924. 'finished'=>false,
  3925. 'message'=>$message,
  3926. 'gseq'=>$LEM->currentGroupSeq,
  3927. 'seq'=>$LEM->currentGroupSeq,
  3928. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  3929. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  3930. 'unansweredSQs'=>$result['unansweredSQs'],
  3931. 'invalidSQs'=>$result['invalidSQs'],
  3932. );
  3933. return $LEM->lastMoveResult;
  3934. }
  3935. }
  3936. break;
  3937. case 'question':
  3938. $LEM->StartProcessingPage();
  3939. $updatedValues=$LEM->ProcessCurrentResponses();
  3940. $message = '';
  3941. while (true)
  3942. {
  3943. $LEM->currentQset = array(); // reset active list of questions
  3944. if (--$LEM->currentQuestionSeq < 0)
  3945. {
  3946. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  3947. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  3948. $LEM->lastMoveResult = array(
  3949. 'at_start'=>true,
  3950. 'finished'=>false,
  3951. 'message'=>$message,
  3952. 'unansweredSQs'=>(isset($result['unansweredSQs']) ? $result['unansweredSQs'] : ''),
  3953. 'invalidSQs'=>(isset($result['invalidSQs']) ? $result['invalidSQs'] : ''),
  3954. );
  3955. return $LEM->lastMoveResult;
  3956. }
  3957. // Set certain variables normally set by StartProcessingGroup()
  3958. $LEM->groupRelevanceInfo=array(); // TODO only important thing from StartProcessingGroup?
  3959. $qInfo = $LEM->questionSeq2relevance[$LEM->currentQuestionSeq];
  3960. $LEM->currentQID=$qInfo['qid'];
  3961. $LEM->currentGroupSeq=$qInfo['gseq'];
  3962. if ($LEM->currentGroupSeq > $LEM->maxGroupSeq) {
  3963. $LEM->maxGroupSeq = $LEM->currentGroupSeq;
  3964. }
  3965. $LEM->ProcessAllNeededRelevance($LEM->currentQuestionSeq);
  3966. $LEM->_CreateSubQLevelRelevanceAndValidationEqns($LEM->currentQuestionSeq);
  3967. $result = $LEM->_ValidateQuestion($LEM->currentQuestionSeq);
  3968. $message .= $result['message'];
  3969. $gRelInfo = $LEM->gRelInfo[$LEM->currentGroupSeq];
  3970. $grel = $gRelInfo['result'];
  3971. if (!$grel || !$result['relevant'] || $result['hidden'])
  3972. {
  3973. // then skip this question - assume already saved?
  3974. continue;
  3975. }
  3976. else
  3977. {
  3978. // display new question
  3979. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  3980. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  3981. return array(
  3982. 'at_start'=>false,
  3983. 'finished'=>false,
  3984. 'message'=>$message,
  3985. 'gseq'=>$LEM->currentGroupSeq,
  3986. 'seq'=>$LEM->currentQuestionSeq,
  3987. 'qseq'=>$LEM->currentQuestionSeq,
  3988. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  3989. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  3990. 'unansweredSQs'=>$result['unansweredSQs'],
  3991. 'invalidSQs'=>$result['invalidSQs'],
  3992. );
  3993. }
  3994. }
  3995. break;
  3996. }
  3997. }
  3998. /**
  3999. *
  4000. * @param <type> $force - if true, continue to go forward even if there are violations to the mandatory and/or validity rules
  4001. */
  4002. static function NavigateForwards($force=false) {
  4003. $now = microtime(true);
  4004. $LEM =& LimeExpressionManager::singleton();
  4005. $LEM->ParseResultCache=array(); // to avoid running same test more than once for a given group
  4006. $LEM->updatedValues = array();
  4007. switch ($LEM->surveyMode)
  4008. {
  4009. case 'survey':
  4010. $startingGroup = $LEM->currentGroupSeq;
  4011. $LEM->StartProcessingPage(true);
  4012. $updatedValues=$LEM->ProcessCurrentResponses();
  4013. $message = '';
  4014. $LEM->currentQset = array(); // reset active list of questions
  4015. $result = $LEM->_ValidateSurvey();
  4016. $message .= $result['message'];
  4017. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4018. if (!$force && !is_null($result) && ($result['mandViolation'] || !$result['valid'] || $startingGroup == -1))
  4019. {
  4020. $finished=false;
  4021. }
  4022. else
  4023. {
  4024. $finished = true;
  4025. }
  4026. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,$finished);
  4027. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4028. $LEM->lastMoveResult = array(
  4029. 'finished'=>$finished,
  4030. 'message'=>$message,
  4031. 'gseq'=>1,
  4032. 'seq'=>1,
  4033. 'mandViolation'=>$result['mandViolation'],
  4034. 'valid'=>$result['valid'],
  4035. 'unansweredSQs'=>$result['unansweredSQs'],
  4036. 'invalidSQs'=>$result['invalidSQs'],
  4037. );
  4038. return $LEM->lastMoveResult;
  4039. break;
  4040. case 'group':
  4041. // First validate the current group
  4042. $LEM->StartProcessingPage();
  4043. $updatedValues=$LEM->ProcessCurrentResponses();
  4044. $message = '';
  4045. if (!$force && $LEM->currentGroupSeq != -1)
  4046. {
  4047. $result = $LEM->_ValidateGroup($LEM->currentGroupSeq);
  4048. $message .= $result['message'];
  4049. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4050. if (!is_null($result) && ($result['mandViolation'] || !$result['valid']))
  4051. {
  4052. // redisplay the current group
  4053. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4054. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4055. $LEM->lastMoveResult = array(
  4056. 'finished'=>false,
  4057. 'message'=>$message,
  4058. 'gseq'=>$LEM->currentGroupSeq,
  4059. 'seq'=>$LEM->currentGroupSeq,
  4060. 'mandViolation'=>$result['mandViolation'],
  4061. 'valid'=>$result['valid'],
  4062. 'unansweredSQs'=>$result['unansweredSQs'],
  4063. 'invalidSQs'=>$result['invalidSQs'],
  4064. );
  4065. return $LEM->lastMoveResult;
  4066. }
  4067. }
  4068. while (true)
  4069. {
  4070. $LEM->currentQset = array(); // reset active list of questions
  4071. if (++$LEM->currentGroupSeq >= $LEM->numGroups)
  4072. {
  4073. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,true);
  4074. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4075. $LEM->lastMoveResult = array(
  4076. 'finished'=>true,
  4077. 'message'=>$message,
  4078. 'gseq'=>$LEM->currentGroupSeq,
  4079. 'seq'=>$LEM->currentGroupSeq,
  4080. 'mandViolation'=>(isset($result['mandViolation']) ? $result['mandViolation'] : false),
  4081. 'valid'=>(isset($result['valid']) ? $result['valid'] : false),
  4082. 'unansweredSQs'=>(isset($result['unansweredSQs']) ? $result['unansweredSQs'] : ''),
  4083. 'invalidSQs'=>(isset($result['invalidSQs']) ? $result['invalidSQs'] : ''),
  4084. );
  4085. return $LEM->lastMoveResult;
  4086. }
  4087. $result = $LEM->_ValidateGroup($LEM->currentGroupSeq);
  4088. if (is_null($result)) {
  4089. continue; // this is an invalid group - skip it
  4090. }
  4091. $message .= $result['message'];
  4092. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4093. if (!$result['relevant'] || $result['hidden'])
  4094. {
  4095. // then skip this group
  4096. continue;
  4097. }
  4098. else
  4099. {
  4100. // display new group
  4101. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4102. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4103. $LEM->lastMoveResult = array(
  4104. 'finished'=>false,
  4105. 'message'=>$message,
  4106. 'gseq'=>$LEM->currentGroupSeq,
  4107. 'seq'=>$LEM->currentGroupSeq,
  4108. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4109. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4110. 'unansweredSQs'=>$result['unansweredSQs'],
  4111. 'invalidSQs'=>$result['invalidSQs'],
  4112. );
  4113. return $LEM->lastMoveResult;
  4114. }
  4115. }
  4116. break;
  4117. case 'question':
  4118. $LEM->StartProcessingPage();
  4119. $updatedValues=$LEM->ProcessCurrentResponses();
  4120. $message = '';
  4121. if (!$force && $LEM->currentQuestionSeq != -1)
  4122. {
  4123. $result = $LEM->_ValidateQuestion($LEM->currentQuestionSeq);
  4124. $message .= $result['message'];
  4125. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4126. $gRelInfo = $LEM->gRelInfo[$LEM->currentGroupSeq];
  4127. $grel = $gRelInfo['result'];
  4128. if ($grel && !is_null($result) && ($result['mandViolation'] || !$result['valid']))
  4129. {
  4130. // redisplay the current question
  4131. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4132. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4133. $LEM->lastMoveResult = array(
  4134. 'finished'=>false,
  4135. 'message'=>$message,
  4136. 'qseq'=>$LEM->currentQuestionSeq,
  4137. 'gseq'=>$LEM->currentGroupSeq,
  4138. 'seq'=>$LEM->currentQuestionSeq,
  4139. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4140. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4141. 'unansweredSQs'=>$result['unansweredSQs'],
  4142. 'invalidSQs'=>$result['invalidSQs'],
  4143. );
  4144. return $LEM->lastMoveResult;
  4145. }
  4146. }
  4147. while (true)
  4148. {
  4149. $LEM->currentQset = array(); // reset active list of questions
  4150. if (++$LEM->currentQuestionSeq >= $LEM->numQuestions)
  4151. {
  4152. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,true);
  4153. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4154. $LEM->lastMoveResult = array(
  4155. 'finished'=>true,
  4156. 'message'=>$message,
  4157. 'qseq'=>$LEM->currentQuestionSeq,
  4158. 'gseq'=>$LEM->currentGroupSeq,
  4159. 'seq'=>$LEM->currentQuestionSeq,
  4160. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4161. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4162. 'unansweredSQs'=>(isset($result['unansweredSQs']) ? $result['unansweredSQs'] : ''),
  4163. 'invalidSQs'=>(isset($result['invalidSQs']) ? $result['invalidSQs'] : ''),
  4164. );
  4165. return $LEM->lastMoveResult;
  4166. }
  4167. // Set certain variables normally set by StartProcessingGroup()
  4168. $LEM->groupRelevanceInfo=array(); // TODO only important thing from StartProcessingGroup?
  4169. $qInfo = $LEM->questionSeq2relevance[$LEM->currentQuestionSeq];
  4170. $LEM->currentQID=$qInfo['qid'];
  4171. $LEM->currentGroupSeq=$qInfo['gseq'];
  4172. if ($LEM->currentGroupSeq > $LEM->maxGroupSeq) {
  4173. $LEM->maxGroupSeq = $LEM->currentGroupSeq;
  4174. }
  4175. $LEM->ProcessAllNeededRelevance($LEM->currentQuestionSeq);
  4176. $LEM->_CreateSubQLevelRelevanceAndValidationEqns($LEM->currentQuestionSeq);
  4177. $result = $LEM->_ValidateQuestion($LEM->currentQuestionSeq);
  4178. $message .= $result['message'];
  4179. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4180. $gRelInfo = $LEM->gRelInfo[$LEM->currentGroupSeq];
  4181. $grel = $gRelInfo['result'];
  4182. if (!$grel || !$result['relevant'] || $result['hidden'])
  4183. {
  4184. // then skip this question - assume already saved?
  4185. continue;
  4186. }
  4187. else
  4188. {
  4189. // display new question
  4190. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4191. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4192. $LEM->lastMoveResult = array(
  4193. 'finished'=>false,
  4194. 'message'=>$message,
  4195. 'qseq'=>$LEM->currentQuestionSeq,
  4196. 'gseq'=>$LEM->currentGroupSeq,
  4197. 'seq'=>$LEM->currentQuestionSeq,
  4198. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4199. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4200. 'unansweredSQs'=>$result['unansweredSQs'],
  4201. 'invalidSQs'=>$result['invalidSQs'],
  4202. );
  4203. return $LEM->lastMoveResult;
  4204. }
  4205. }
  4206. break;
  4207. }
  4208. }
  4209. /**
  4210. * Write values to database.
  4211. * @param <type> $updatedValues
  4212. * @param <boolean> $finished - true if the survey needs to be finalized
  4213. */
  4214. private function _UpdateValuesInDatabase($updatedValues, $finished=false)
  4215. {
  4216. // TODO - now that using $this->updatedValues, may be able to remove local copies of it (unless needed by other sub-systems)
  4217. $updatedValues = $this->updatedValues;
  4218. if (!$this->surveyOptions['deletenonvalues'])
  4219. {
  4220. $nonNullValues = array();
  4221. foreach($updatedValues as $key=>$value)
  4222. {
  4223. if (!is_null($value))
  4224. {
  4225. if (isset($value['value']) && !is_null($value['value']))
  4226. {
  4227. $nonNullValues[$key] = $value;
  4228. }
  4229. }
  4230. }
  4231. $updatedValues = $nonNullValues;
  4232. }
  4233. $message = '';
  4234. $_SESSION[$this->sessid]['datestamp']=dateShift(date("Y-m-d H:i:s"), "Y-m-d H:i:s", $this->surveyOptions['timeadjust']);
  4235. if ($this->surveyOptions['active'] && !isset($_SESSION[$this->sessid]['srid']))
  4236. {
  4237. // Create initial insert row for this record
  4238. $today = dateShift(date("Y-m-d H:i:s"), "Y-m-d H:i:s", $this->surveyOptions['timeadjust']);
  4239. $sdata = array(
  4240. "startlanguage"=>$this->surveyOptions['startlanguage'],
  4241. );
  4242. if ($this->surveyOptions['anonymized'] == false)
  4243. {
  4244. $sdata = array_merge($sdata,array("token"=>($this->surveyOptions['token'])));
  4245. }
  4246. if ($this->surveyOptions['datestamp'] == true)
  4247. {
  4248. $sdata = array_merge($sdata, array(
  4249. "datestamp"=>($this->surveyOptions['datestamp'] ? $_SESSION[$this->sessid]['datestamp'] : NULL),
  4250. "startdate"=>($this->surveyOptions['datestamp'] ? $_SESSION[$this->sessid]['datestamp'] : date("Y-m-d H:i:s",0))
  4251. ));
  4252. }
  4253. if ($this->surveyOptions['ipaddr'] == true)
  4254. {
  4255. $sdata = array_merge($sdata,array("ipaddr"=>getIPAddress()));
  4256. }
  4257. if ($this->surveyOptions['refurl'] == true)
  4258. {
  4259. $sdata = array_merge($sdata,array("refurl"=>(($this->surveyOptions['refurl']) ? getenv("HTTP_REFERER") : NULL)));
  4260. }
  4261. $sdata = array_filter($sdata);
  4262. Survey_dynamic::sid($this->sid);
  4263. $oSurvey = new Survey_dynamic;
  4264. $iNewID = $oSurvey->insertRecords($sdata);
  4265. if ($iNewID) // Checked
  4266. {
  4267. $srid = $iNewID;
  4268. $_SESSION[$this->sessid]['srid'] = $iNewID;
  4269. }
  4270. else
  4271. {
  4272. $message .= $this->gT("Unable to insert record into survey table"); // TODO - add SQL error?
  4273. }
  4274. //Insert Row for Timings, if needed
  4275. if ($this->surveyOptions['savetimings']) {
  4276. Survey_timings::sid($this->sid);
  4277. $oSurveyTimings = new Survey_timings;
  4278. $tdata = array(
  4279. 'id'=>$srid,
  4280. 'interviewtime'=>0
  4281. );
  4282. switchMSSQLIdentityInsert("survey_{$this->sid}_timings", true);
  4283. $iNewID = $oSurveyTimings->insertRecords($tdata);
  4284. switchMSSQLIdentityInsert("survey_{$this->sid}_timings", false);
  4285. }
  4286. }
  4287. if (count($updatedValues) > 0 || $finished)
  4288. {
  4289. $query = 'UPDATE ' . $this->surveyOptions['tablename'] . ' SET ';
  4290. $setter = array();
  4291. switch ($this->surveyMode)
  4292. {
  4293. case 'question':
  4294. $thisstep = $this->currentQuestionSeq;
  4295. break;
  4296. case 'group':
  4297. $thisstep = $this->currentGroupSeq;
  4298. break;
  4299. case 'survey':
  4300. $thisstep = 1;
  4301. break;
  4302. }
  4303. $setter[] = dbQuoteID('lastpage') . "=" . dbQuoteAll($thisstep);
  4304. if ($this->surveyOptions['datestamp'] && isset($_SESSION[$this->sessid]['datestamp'])) {
  4305. $setter[] = dbQuoteID('datestamp') . "=" . dbQuoteAll($_SESSION[$this->sessid]['datestamp']);
  4306. }
  4307. if ($this->surveyOptions['ipaddr']) {
  4308. $setter[] = dbQuoteID('ipaddr') . "=" . dbQuoteAll(getIPAddress());
  4309. }
  4310. foreach ($updatedValues as $key=>$value)
  4311. {
  4312. $val = (is_null($value) ? NULL : $value['value']);
  4313. $type = (is_null($value) ? NULL : $value['type']);
  4314. // Clean up the values to cope with database storage requirements
  4315. switch($type)
  4316. {
  4317. case 'D': //DATE
  4318. if (trim($val)=='') {
  4319. $val=NULL; // since some databases can't store blanks in date fields
  4320. }
  4321. // otherwise will already be in yyyy-mm-dd format after ProcessCurrentResponses()
  4322. break;
  4323. case '|': //File upload
  4324. // This block can be removed once we require 5.3 or later
  4325. if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
  4326. $val=addslashes($val);
  4327. }
  4328. break;
  4329. case 'N': //NUMERICAL QUESTION TYPE
  4330. case 'K': //MULTIPLE NUMERICAL QUESTION
  4331. if (trim($val)=='') {
  4332. $val=NULL; // since some databases can't store blanks in numerical inputs
  4333. }
  4334. break;
  4335. default:
  4336. break;
  4337. }
  4338. if (is_null($val))
  4339. {
  4340. $setter[] = dbQuoteID($key) . "=NULL";
  4341. }
  4342. else
  4343. {
  4344. $setter[] = dbQuoteID($key) . "=" . dbQuoteAll($val);
  4345. }
  4346. }
  4347. $query .= implode(', ', $setter);
  4348. $query .= " WHERE ID=";
  4349. if (isset($_SESSION[$this->sessid]['srid']) && $this->surveyOptions['active'])
  4350. {
  4351. $query .= $_SESSION[$this->sessid]['srid'];
  4352. if (!dbExecuteAssoc($query))
  4353. {
  4354. echo submitfailed(''); // TODO - report SQL error?
  4355. if (($this->debugLevel & LEM_DEBUG_VALIDATION_SUMMARY) == LEM_DEBUG_VALIDATION_SUMMARY) {
  4356. $message .= $this->gT('Error in SQL update'); // TODO - add SQL error?
  4357. }
  4358. }
  4359. // Save Timings if needed
  4360. if ($this->surveyOptions['savetimings']) {
  4361. Yii::import("application.libraries.Save");
  4362. $cSave = new Save();
  4363. $cSave->set_answer_time();
  4364. }
  4365. if ($finished)
  4366. {
  4367. // Delete the save control record if successfully finalize the submission
  4368. $query = "DELETE FROM {{saved_control}} where srid=".$_SESSION[$this->sessid]['srid'].' and sid='.$this->sid;
  4369. Yii::app()->db->createCommand($query)->execute();
  4370. if (($this->debugLevel & LEM_DEBUG_VALIDATION_SUMMARY) == LEM_DEBUG_VALIDATION_SUMMARY) {
  4371. $message .= ';<br />'.$query;
  4372. }
  4373. }
  4374. else if ($this->surveyOptions['allowsave'] && isset($_SESSION[$this->sessid]['scid']))
  4375. {
  4376. Saved_control::model()->updateByPk($_SESSION[$this->sessid]['scid'], array('saved_thisstep'=>$thisstep));
  4377. }
  4378. // Check Quotas
  4379. $bQuotaMatched = false;
  4380. $aQuotas = checkQuota('return', $this->sid);
  4381. if ($aQuotas !== false)
  4382. {
  4383. if ($aQuotas != false)
  4384. {
  4385. foreach ($aQuotas as $aQuota)
  4386. {
  4387. if (isset($aQuota['status']) && $aQuota['status'] == 'matched') {
  4388. $bQuotaMatched = true;
  4389. }
  4390. }
  4391. }
  4392. }
  4393. if ($bQuotaMatched)
  4394. {
  4395. checkQuota('enforce',$this->sid); // will create a page and quit.
  4396. }
  4397. else
  4398. {
  4399. if ($finished) {
  4400. $sQuery = 'UPDATE '.$this->surveyOptions['tablename'] . " SET ";
  4401. if($this->surveyOptions['datestamp'])
  4402. {
  4403. // Replace with date("Y-m-d H:i:s") ? See timeadjust
  4404. $sQuery .= dbQuoteID('submitdate') . "=" . dbQuoteAll($_SESSION[$this->sessid]['datestamp']);
  4405. }
  4406. else
  4407. {
  4408. $sQuery .= dbQuoteID('submitdate') . "=" . dbQuoteAll(date("Y-m-d H:i:s",mktime(0,0,0,1,1,1980)));
  4409. }
  4410. $sQuery .= " WHERE ID=".$_SESSION[$this->sessid]['srid'];
  4411. dbExecuteAssoc($sQuery); // Checked
  4412. }
  4413. }
  4414. }
  4415. if (($this->debugLevel & LEM_DEBUG_VALIDATION_SUMMARY) == LEM_DEBUG_VALIDATION_SUMMARY) {
  4416. $message .= $query;
  4417. }
  4418. }
  4419. return $message;
  4420. }
  4421. /**
  4422. * Get last move information, optionally clearing the substitution cache
  4423. * @param type $clearSubstitutionInfo
  4424. * @return type
  4425. */
  4426. static function GetLastMoveResult($clearSubstitutionInfo=false)
  4427. {
  4428. $LEM =& LimeExpressionManager::singleton();
  4429. if ($clearSubstitutionInfo)
  4430. {
  4431. $LEM->em->ClearSubstitutionInfo(); // need to avoid double-generation of tailoring info
  4432. }
  4433. return (isset($LEM->lastMoveResult) ? $LEM->lastMoveResult : NULL);
  4434. }
  4435. /**
  4436. * Jump to a specific question or group sequence. If jumping forward, it re-validates everything in between
  4437. * @param <type> $seq
  4438. * @param <type> $force - if true, then skip validation of current group (e.g. will jump even if there are errors)
  4439. * @param <type> $preview - if true, then treat this group/question as relevant, even if it is not, so that it can be displayed
  4440. * @return <type>
  4441. */
  4442. static function JumpTo($seq,$preview=false,$processPOST=true,$force=false,$changeLang=false) {
  4443. $now = microtime(true);
  4444. $LEM =& LimeExpressionManager::singleton();
  4445. if ($changeLang)
  4446. {
  4447. $LEM->setVariableAndTokenMappingsForExpressionManager($LEM->sid,true,$LEM->surveyOptions['anonymized'],$LEM->allOnOnePage);
  4448. }
  4449. $LEM->ParseResultCache=array(); // to avoid running same test more than once for a given group
  4450. $LEM->updatedValues = array();
  4451. --$seq; // convert to 0-based numbering
  4452. switch ($LEM->surveyMode)
  4453. {
  4454. case 'survey':
  4455. // This only happens if saving data so far, so don't want to submit it, just validate and return
  4456. $startingGroup = $LEM->currentGroupSeq;
  4457. $LEM->StartProcessingPage(true);
  4458. if ($processPOST) {
  4459. $updatedValues=$LEM->ProcessCurrentResponses();
  4460. }
  4461. else {
  4462. $updatedValues = array();
  4463. }
  4464. $message = '';
  4465. $LEM->currentQset = array(); // reset active list of questions
  4466. $result = $LEM->_ValidateSurvey();
  4467. $message .= $result['message'];
  4468. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4469. $finished=false;
  4470. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,$finished);
  4471. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4472. $LEM->lastMoveResult = array(
  4473. 'finished'=>$finished,
  4474. 'message'=>$message,
  4475. 'gseq'=>1,
  4476. 'seq'=>1,
  4477. 'mandViolation'=>$result['mandViolation'],
  4478. 'valid'=>$result['valid'],
  4479. 'unansweredSQs'=>$result['unansweredSQs'],
  4480. 'invalidSQs'=>$result['invalidSQs'],
  4481. );
  4482. return $LEM->lastMoveResult;
  4483. break;
  4484. case 'group':
  4485. // First validate the current group
  4486. $LEM->StartProcessingPage();
  4487. if ($processPOST) {
  4488. $updatedValues=$LEM->ProcessCurrentResponses();
  4489. }
  4490. else {
  4491. $updatedValues = array();
  4492. }
  4493. $message = '';
  4494. if (!$force && $LEM->currentGroupSeq != -1 && $seq > $LEM->currentGroupSeq) // only re-validate if jumping forward
  4495. {
  4496. $result = $LEM->_ValidateGroup($LEM->currentGroupSeq);
  4497. $message .= $result['message'];
  4498. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4499. if (!is_null($result) && ($result['mandViolation'] || !$result['valid']))
  4500. {
  4501. // redisplay the current group
  4502. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4503. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4504. $LEM->lastMoveResult = array(
  4505. 'finished'=>false,
  4506. 'message'=>$message,
  4507. 'gseq'=>$LEM->currentGroupSeq,
  4508. 'seq'=>$LEM->currentGroupSeq,
  4509. 'mandViolation'=>$result['mandViolation'],
  4510. 'valid'=>$result['valid'],
  4511. 'unansweredSQs'=>$result['unansweredSQs'],
  4512. 'invalidSQs'=>$result['invalidSQs'],
  4513. );
  4514. return $LEM->lastMoveResult;
  4515. }
  4516. }
  4517. if ($seq <= $LEM->currentGroupSeq || $preview) {
  4518. $LEM->currentGroupSeq = $seq-1; // Try to jump to the requested group, but navigate to next if needed
  4519. }
  4520. while (true)
  4521. {
  4522. $LEM->currentQset = array(); // reset active list of questions
  4523. if (++$LEM->currentGroupSeq >= $LEM->numGroups)
  4524. {
  4525. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,true);
  4526. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4527. $LEM->lastMoveResult = array(
  4528. 'finished'=>true,
  4529. 'message'=>$message,
  4530. 'gseq'=>$LEM->currentGroupSeq,
  4531. 'seq'=>$LEM->currentGroupSeq,
  4532. 'mandViolation'=>(isset($result['mandViolation']) ? $result['mandViolation'] : false),
  4533. 'valid'=>(isset($result['valid']) ? $result['valid'] : false),
  4534. 'unansweredSQs'=>(isset($result['unansweredSQs']) ? $result['unansweredSQs'] : ''),
  4535. 'invalidSQs'=>(isset($result['invalidSQs']) ? $result['invalidSQs'] : ''),
  4536. );
  4537. return $LEM->lastMoveResult;
  4538. }
  4539. $result = $LEM->_ValidateGroup($LEM->currentGroupSeq);
  4540. if (is_null($result)) {
  4541. return NULL; // invalid group - either bad number, or no questions within it
  4542. }
  4543. $message .= $result['message'];
  4544. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4545. if (!$preview && (!$result['relevant'] || $result['hidden']))
  4546. {
  4547. // then skip this group - assume already saved?
  4548. continue;
  4549. }
  4550. elseif (!($result['mandViolation'] || !$result['valid']) && $LEM->currentGroupSeq < $seq) {
  4551. // if there is a violation while moving forward, need to stop and ask that set of questions
  4552. // if there are no violations, can skip this group as long as changed values are saved.
  4553. continue;
  4554. }
  4555. else
  4556. {
  4557. // display new group
  4558. if(!$preview){ // Save only if not in preview mode
  4559. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4560. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4561. }
  4562. $LEM->lastMoveResult = array(
  4563. 'finished'=>false,
  4564. 'message'=>$message,
  4565. 'gseq'=>$LEM->currentGroupSeq,
  4566. 'seq'=>$LEM->currentGroupSeq,
  4567. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4568. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4569. 'unansweredSQs'=>$result['unansweredSQs'],
  4570. 'invalidSQs'=>$result['invalidSQs'],
  4571. );
  4572. return $LEM->lastMoveResult;
  4573. }
  4574. }
  4575. break;
  4576. case 'question':
  4577. $LEM->StartProcessingPage();
  4578. if ($processPOST) {
  4579. $updatedValues=$LEM->ProcessCurrentResponses();
  4580. }
  4581. else {
  4582. $updatedValues = array();
  4583. }
  4584. $message = '';
  4585. if (!$force && $LEM->currentQuestionSeq != -1 && $seq > $LEM->currentQuestionSeq)
  4586. {
  4587. $result = $LEM->_ValidateQuestion($LEM->currentQuestionSeq);
  4588. $message .= $result['message'];
  4589. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4590. $gRelInfo = $LEM->gRelInfo[$LEM->currentGroupSeq];
  4591. $grel = $gRelInfo['result'];
  4592. if ($grel && ($result['mandViolation'] || !$result['valid']))
  4593. {
  4594. // redisplay the current question
  4595. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4596. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4597. $LEM->lastMoveResult = array(
  4598. 'finished'=>false,
  4599. 'message'=>$message,
  4600. 'qseq'=>$LEM->currentQuestionSeq,
  4601. 'gseq'=>$LEM->currentGroupSeq,
  4602. 'seq'=>$LEM->currentQuestionSeq,
  4603. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4604. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4605. 'unansweredSQs'=>$result['unansweredSQs'],
  4606. 'invalidSQs'=>$result['invalidSQs'],
  4607. );
  4608. return $LEM->lastMoveResult;
  4609. }
  4610. }
  4611. if ($seq <= $LEM->currentQuestionSeq || $preview) {
  4612. $LEM->currentQuestionSeq = $seq-1; // Try to jump to the requested group, but navigate to next if needed
  4613. }
  4614. while (true)
  4615. {
  4616. $LEM->currentQset = array(); // reset active list of questions
  4617. if (++$LEM->currentQuestionSeq >= $LEM->numQuestions)
  4618. {
  4619. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,true);
  4620. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4621. $LEM->lastMoveResult = array(
  4622. 'finished'=>true,
  4623. 'message'=>$message,
  4624. 'qseq'=>$LEM->currentQuestionSeq,
  4625. 'gseq'=>$LEM->currentGroupSeq,
  4626. 'seq'=>$LEM->currentQuestionSeq,
  4627. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4628. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4629. 'unansweredSQs'=>(isset($result['unansweredSQs']) ? $result['unansweredSQs'] : ''),
  4630. 'invalidSQs'=>(isset($result['invalidSQs']) ? $result['invalidSQs'] : ''),
  4631. );
  4632. return $LEM->lastMoveResult;
  4633. }
  4634. // Set certain variables normally set by StartProcessingGroup()
  4635. $LEM->groupRelevanceInfo=array(); // TODO only important thing from StartProcessingGroup?
  4636. if (!isset($LEM->questionSeq2relevance[$LEM->currentQuestionSeq])) {
  4637. return NULL; // means an invalid question - probably no sub-quetions
  4638. }
  4639. $qInfo = $LEM->questionSeq2relevance[$LEM->currentQuestionSeq];
  4640. $LEM->currentQID=$qInfo['qid'];
  4641. $LEM->currentGroupSeq=$qInfo['gseq'];
  4642. if ($LEM->currentGroupSeq > $LEM->maxGroupSeq) {
  4643. $LEM->maxGroupSeq = $LEM->currentGroupSeq;
  4644. }
  4645. $LEM->ProcessAllNeededRelevance($LEM->currentQuestionSeq);
  4646. $LEM->_CreateSubQLevelRelevanceAndValidationEqns($LEM->currentQuestionSeq);
  4647. $result = $LEM->_ValidateQuestion($LEM->currentQuestionSeq);
  4648. $message .= $result['message'];
  4649. $updatedValues = array_merge($updatedValues,$result['updatedValues']);
  4650. $gRelInfo = $LEM->gRelInfo[$LEM->currentGroupSeq];
  4651. $grel = $gRelInfo['result'];
  4652. if (!$preview && (!$grel || !$result['relevant'] || $result['hidden']))
  4653. {
  4654. // then skip this question
  4655. continue;
  4656. }
  4657. else if (!$preview && !$grel)
  4658. {
  4659. continue;
  4660. }
  4661. else if (!$preview && !($result['mandViolation'] || !$result['valid']) && $LEM->currentQuestionSeq < $seq)
  4662. {
  4663. // if there is a violation while moving forward, need to stop and ask that set of questions
  4664. // if there are no violations, can skip this group as long as changed values are saved.
  4665. continue;
  4666. }
  4667. else
  4668. {
  4669. // display new question
  4670. $message .= $LEM->_UpdateValuesInDatabase($updatedValues,false);
  4671. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  4672. $LEM->lastMoveResult = array(
  4673. 'finished'=>false,
  4674. 'message'=>$message,
  4675. 'qseq'=>$LEM->currentQuestionSeq,
  4676. 'gseq'=>$LEM->currentGroupSeq,
  4677. 'seq'=>$LEM->currentQuestionSeq,
  4678. 'mandViolation'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['mandViolation'] : false),
  4679. 'valid'=> (($LEM->maxGroupSeq > $LEM->currentGroupSeq) ? $result['valid'] : false),
  4680. 'unansweredSQs'=>$result['unansweredSQs'],
  4681. 'invalidSQs'=>$result['invalidSQs'],
  4682. );
  4683. return $LEM->lastMoveResult;
  4684. }
  4685. }
  4686. break;
  4687. }
  4688. }
  4689. /**
  4690. * Check the entire survey
  4691. * @return <type>
  4692. */
  4693. private function _ValidateSurvey()
  4694. {
  4695. $LEM =& $this;
  4696. $message = '';
  4697. $srel=false;
  4698. $shidden=true;
  4699. $smandViolation=false;
  4700. $svalid=true;
  4701. $unansweredSQs = array();
  4702. $invalidSQs = array();
  4703. $updatedValues = array();
  4704. $sanyUnanswered = false;
  4705. ///////////////////////////////////////////////////////
  4706. // CHECK EACH GROUP, AND SET SURVEY-LEVEL PROPERTIES //
  4707. ///////////////////////////////////////////////////////
  4708. for ($i=0;$i<$LEM->numGroups;++$i) {
  4709. $LEM->currentGroupSeq=$i;
  4710. $gStatus = $LEM->_ValidateGroup($i);
  4711. if (is_null($gStatus)) {
  4712. continue; // invalid group, so skip it
  4713. }
  4714. $message .= $gStatus['message'];
  4715. if ($gStatus['relevant']) {
  4716. $srel = true;
  4717. }
  4718. if ($gStatus['relevant'] && !$gStatus['hidden']) {
  4719. $shidden=false;
  4720. }
  4721. if ($gStatus['relevant'] && !$gStatus['hidden'] && $gStatus['mandViolation']) {
  4722. $smandViolation = true;
  4723. }
  4724. if ($gStatus['relevant'] && !$gStatus['hidden'] && !$gStatus['valid']) {
  4725. $svalid=false;
  4726. }
  4727. if ($gStatus['anyUnanswered']) {
  4728. $sanyUnanswered = true;
  4729. }
  4730. if (strlen($gStatus['unansweredSQs']) > 0) {
  4731. $unansweredSQs = array_merge($unansweredSQs, explode('|',$gStatus['unansweredSQs']));
  4732. }
  4733. if (strlen($gStatus['invalidSQs']) > 0) {
  4734. $invalidSQs = array_merge($invalidSQs, explode('|',$gStatus['invalidSQs']));
  4735. }
  4736. $updatedValues = array_merge($updatedValues, $gStatus['updatedValues']);
  4737. // array_merge destroys the key, so do it manually
  4738. foreach ($gStatus['qset'] as $key=>$value) {
  4739. $LEM->currentQset[$key] = $value;
  4740. }
  4741. $LEM->FinishProcessingGroup();
  4742. }
  4743. return array(
  4744. 'relevant' => $srel,
  4745. 'hidden' => $shidden,
  4746. 'mandViolation' => $smandViolation,
  4747. 'valid' => $svalid,
  4748. 'anyUnanswered' => $sanyUnanswered,
  4749. 'message' => $message,
  4750. 'unansweredSQs' => implode('|',$unansweredSQs),
  4751. 'invalidSQs' => implode('|',$invalidSQs),
  4752. 'updatedValues' => $updatedValues,
  4753. 'seq'=>1,
  4754. );
  4755. }
  4756. /**
  4757. * Check a group and all of the questions it contains
  4758. * @param <type> $groupSeq - the index-0 sequence number for this group
  4759. * @return <array> - detailed information about this group
  4760. */
  4761. function _ValidateGroup($groupSeq)
  4762. {
  4763. $LEM =& $this;
  4764. if ($groupSeq < 0 || $groupSeq >= $LEM->numGroups) {
  4765. return NULL; // TODO - what is desired behavior?
  4766. }
  4767. $groupSeqInfo = (isset($LEM->groupSeqInfo[$groupSeq]) ? $LEM->groupSeqInfo[$groupSeq] : NULL);
  4768. if (is_null($groupSeqInfo)) {
  4769. // then there are no questions in this group
  4770. return NULL;
  4771. }
  4772. $qInfo = $LEM->questionSeq2relevance[$groupSeqInfo['qstart']];
  4773. $gseq = $qInfo['gseq'];
  4774. $gid = $qInfo['gid'];
  4775. $LEM->StartProcessingGroup($gseq, $LEM->surveyOptions['anonymized'], $LEM->sid); // analyze the data we have about this group
  4776. $grel=false; // assume irrelevant until find a relevant question
  4777. $ghidden=true; // assume hidden until find a non-hidden question. If there are no relevant questions on this page, $ghidden will stay true
  4778. $gmandViolation=false; // assume that the group contains no manditory questions that have not been fully answered
  4779. $gvalid=true; // assume valid until discover otherwise
  4780. $debug_message = '';
  4781. $messages = array();
  4782. $currentQset = array();
  4783. $unansweredSQs = array();
  4784. $invalidSQs = array();
  4785. $updatedValues = array();
  4786. $ganyUnanswered = false;
  4787. $gRelInfo = $LEM->gRelInfo[$groupSeq];
  4788. /////////////////////////////////////////////////////////
  4789. // CHECK EACH QUESTION, AND SET GROUP-LEVEL PROPERTIES //
  4790. /////////////////////////////////////////////////////////
  4791. for ($i=$groupSeqInfo['qstart'];$i<=$groupSeqInfo['qend']; ++$i)
  4792. {
  4793. $qStatus = $LEM->_ValidateQuestion($i);
  4794. $updatedValues = array_merge($updatedValues,$qStatus['updatedValues']);
  4795. if ($gRelInfo['result']==true && $qStatus['relevant']==true) {
  4796. $grel = $gRelInfo['result']; // true; // at least one question relevant
  4797. }
  4798. if ($qStatus['hidden']==false && $qStatus['relevant'] == true) {
  4799. $ghidden=false; // at least one question is visible
  4800. }
  4801. if ($qStatus['relevant']==true && $qStatus['hidden']==false && $qStatus['mandViolation']==true) {
  4802. $gmandViolation=true; // at least one relevant question fails mandatory test
  4803. }
  4804. if ($qStatus['anyUnanswered']==true) {
  4805. $ganyUnanswered=true;
  4806. }
  4807. if ($qStatus['relevant']==true && $qStatus['hidden']==false && $qStatus['valid']==false) {
  4808. $gvalid=false; // at least one question fails validity constraints
  4809. }
  4810. $currentQset[$qStatus['info']['qid']] = $qStatus;
  4811. $messages[] = $qStatus['message'];
  4812. if (strlen($qStatus['unansweredSQs']) > 0) {
  4813. $unansweredSQs[] = $qStatus['unansweredSQs'];
  4814. }
  4815. if (strlen($qStatus['invalidSQs']) > 0) {
  4816. $invalidSQs[] = $qStatus['invalidSQs'];
  4817. }
  4818. }
  4819. $unansweredSQList = implode('|',$unansweredSQs);
  4820. $invalidSQList = implode('|',$invalidSQs);
  4821. /////////////////////////////////////////////////////////
  4822. // OPTIONALLY DISPLAY (DETAILED) DEBUGGING INFORMATION //
  4823. /////////////////////////////////////////////////////////
  4824. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_SUMMARY) == LEM_DEBUG_VALIDATION_SUMMARY)
  4825. {
  4826. $editlink = Yii::app()->getController()->createUrl('admin/survey/sa/view/surveyid/' . $LEM->sid . '/gid/' . $gid);
  4827. $debug_message .= '<br />[G#' . $LEM->currentGroupSeq . ']'
  4828. . '[' . $groupSeqInfo['qstart'] . '-' . $groupSeqInfo['qend'] . ']'
  4829. . "[<a href='$editlink'>"
  4830. . 'GID:' . $gid . "</a>]: "
  4831. . ($grel ? 'relevant ' : " <span style='color:red'>irrelevant</span> ")
  4832. . (($gRelInfo['eqn'] != '') ? $gRelInfo['prettyprint'] : '')
  4833. . (($ghidden && $grel) ? " <span style='color:red'>always-hidden</span> " : ' ')
  4834. . ($gmandViolation ? " <span style='color:red'>(missing a relevant mandatory)</span> " : ' ')
  4835. . ($gvalid ? '' : " <span style='color:red'>(fails at least one validation rule)</span> ")
  4836. . "<br />\n"
  4837. . implode('', $messages);
  4838. if ($grel == true)
  4839. {
  4840. if (!$gvalid)
  4841. {
  4842. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_DETAIL) == LEM_DEBUG_VALIDATION_DETAIL)
  4843. {
  4844. $debug_message .= "**At least one relevant question was invalid, so re-show this group<br />\n";
  4845. $debug_message .= "**Validity Violators: " . implode(', ', explode('|',$invalidSQList)) . "<br />\n";
  4846. }
  4847. }
  4848. if ($gmandViolation)
  4849. {
  4850. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_DETAIL) == LEM_DEBUG_VALIDATION_DETAIL)
  4851. {
  4852. $debug_message .= "**At least one relevant question was mandatory but unanswered, so re-show this group<br />\n";
  4853. $debug_message .= '**Mandatory Violators: ' . implode(', ', explode('|',$unansweredSQList)). "<br />\n";
  4854. }
  4855. }
  4856. if ($ghidden == true)
  4857. {
  4858. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_DETAIL) == LEM_DEBUG_VALIDATION_DETAIL)
  4859. {
  4860. $debug_message .= '** Page is relevant but hidden, so NULL irrelevant values and save relevant Equation results:</br>';
  4861. }
  4862. }
  4863. }
  4864. else
  4865. {
  4866. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_DETAIL) == LEM_DEBUG_VALIDATION_DETAIL)
  4867. {
  4868. $debug_message .= '** Page is irrelevant, so NULL all questions in this group<br />';
  4869. }
  4870. }
  4871. }
  4872. //////////////////////////////////////////////////////////////////////////
  4873. // STORE METADATA NEEDED FOR SUBSEQUENT PROCESSING AND DISPLAY PURPOSES //
  4874. //////////////////////////////////////////////////////////////////////////
  4875. $currentGroupInfo = array(
  4876. 'gseq' => $groupSeq,
  4877. 'message' => $debug_message,
  4878. 'relevant' => $grel,
  4879. 'hidden' => $ghidden,
  4880. 'mandViolation' => $gmandViolation,
  4881. 'valid' => $gvalid,
  4882. 'qset' => $currentQset,
  4883. 'unansweredSQs' => $unansweredSQList,
  4884. 'anyUnanswered' => $ganyUnanswered,
  4885. 'invalidSQs' => $invalidSQList,
  4886. 'updatedValues' => $updatedValues,
  4887. );
  4888. ////////////////////////////////////////////////////////
  4889. // STORE METADATA NEEDED TO GENERATE NAVIGATION INDEX //
  4890. ////////////////////////////////////////////////////////
  4891. $LEM->indexGseq[$groupSeq] = array(
  4892. 'gtext' => $LEM->gseq2info[$groupSeq]['description'],
  4893. 'gname' => $LEM->gseq2info[$groupSeq]['group_name'],
  4894. 'gid' => $LEM->gseq2info[$groupSeq]['gid'], // TODO how used if random?
  4895. 'anyUnanswered' => $ganyUnanswered,
  4896. 'anyErrors' => (($gmandViolation || !$gvalid) ? true : false),
  4897. 'valid' => $gvalid,
  4898. 'mandViolation' => $gmandViolation,
  4899. 'show' => (($grel && !$ghidden) ? true : false),
  4900. );
  4901. $LEM->gseq2relevanceStatus[$gseq] = $grel;
  4902. return $currentGroupInfo;
  4903. }
  4904. /**
  4905. * For the current set of questions (whether in survey, gtoup, or question-by-question mode), assesses the following:
  4906. * (a) mandatory - if so, then all relevant sub-questions must be answered (e.g. pay attention to array_filter and array_filter_exclude)
  4907. * (b) always-hidden
  4908. * (c) relevance status - including sub-question-level relevance
  4909. * (d) answered - if $_SESSION[$LEM->sessid][sgqa]=='' or NULL, then it is not answered
  4910. * (e) validity - whether relevant questions pass their validity tests
  4911. * @param <type> $questionSeq - the 0-index sequence number for this question
  4912. * @return <array> of information about this question and its sub-questions
  4913. */
  4914. function _ValidateQuestion($questionSeq)
  4915. {
  4916. $LEM =& $this;
  4917. $qInfo = $LEM->questionSeq2relevance[$questionSeq]; // this array is by group and question sequence
  4918. $qrel=true; // assume relevant unless discover otherwise
  4919. $prettyPrintRelEqn=''; // assume no relevance eqn by default
  4920. $qid=$qInfo['qid'];
  4921. $gid=$qInfo['gid'];
  4922. $qhidden = $qInfo['hidden'];
  4923. $debug_qmessage='';
  4924. $gRelInfo = $LEM->gRelInfo[$qInfo['gseq']];
  4925. $grel = $gRelInfo['result'];
  4926. ///////////////////////////
  4927. // IS QUESTION RELEVANT? //
  4928. ///////////////////////////
  4929. if (!isset($qInfo['relevance']) || $qInfo['relevance'] == '')
  4930. {
  4931. $relevanceEqn = 1;
  4932. }
  4933. else
  4934. {
  4935. $relevanceEqn = $qInfo['relevance'];
  4936. }
  4937. // cache results
  4938. $relevanceEqn = htmlspecialchars_decode($relevanceEqn,ENT_QUOTES); // TODO is this needed?
  4939. if (isset($LEM->ParseResultCache[$relevanceEqn]))
  4940. {
  4941. $qrel = $LEM->ParseResultCache[$relevanceEqn]['result'];
  4942. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  4943. {
  4944. $prettyPrintRelEqn = $LEM->ParseResultCache[$relevanceEqn]['prettyprint'];
  4945. }
  4946. }
  4947. else
  4948. {
  4949. $qrel = $LEM->em->ProcessBooleanExpression($relevanceEqn,$qInfo['gseq'], $qInfo['qseq']); // assumes safer to re-process relevance and not trust POST values
  4950. $hasErrors = $LEM->em->HasErrors();
  4951. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  4952. {
  4953. $prettyPrintRelEqn = $LEM->em->GetPrettyPrintString();
  4954. }
  4955. $LEM->ParseResultCache[$relevanceEqn] = array(
  4956. 'result'=>$qrel,
  4957. 'prettyprint'=>$prettyPrintRelEqn,
  4958. 'hasErrors'=>$hasErrors,
  4959. );
  4960. }
  4961. //////////////////////////////////////
  4962. // ARE ANY SUB-QUESTION IRRELEVANT? //
  4963. //////////////////////////////////////
  4964. // identify the relevant subquestions (array_filter and array_filter_exclude may make some irrelevant)
  4965. $relevantSQs=array();
  4966. $irrelevantSQs=array();
  4967. $prettyPrintSQRelEqns=array();
  4968. $prettyPrintSQRelEqn='';
  4969. $prettyPrintValidTip='';
  4970. $anyUnanswered = false;
  4971. if (!$qrel)
  4972. {
  4973. // All sub-questions are irrelevant
  4974. $irrelevantSQs = explode('|', $LEM->qid2code[$qid]);
  4975. }
  4976. else
  4977. {
  4978. // Check filter status to determine which subquestions are relevant
  4979. if ($qInfo['type'] == 'X') {
  4980. $sgqas = array(); // Boilerplate questions can be ignored
  4981. }
  4982. else {
  4983. $sgqas = explode('|',$LEM->qid2code[$qid]);
  4984. }
  4985. foreach ($sgqas as $sgqa)
  4986. {
  4987. // for each subq, see if it is part of an array_filter or array_filter_exclude
  4988. if (!isset($LEM->subQrelInfo[$qid]))
  4989. {
  4990. $relevantSQs[] = $sgqa;
  4991. continue;
  4992. }
  4993. $foundSQrelevance=false;
  4994. foreach ($LEM->subQrelInfo[$qid] as $sq)
  4995. {
  4996. switch ($sq['qtype'])
  4997. {
  4998. case '1': //Array (Flexible Labels) dual scale
  4999. if ($sgqa == ($sq['rowdivid'] . '#0') || $sgqa == ($sq['rowdivid'] . '#1')) {
  5000. $foundSQrelevance=true;
  5001. if (isset($LEM->ParseResultCache[$sq['eqn']]))
  5002. {
  5003. $sqrel = $LEM->ParseResultCache[$sq['eqn']]['result'];
  5004. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5005. {
  5006. $prettyPrintSQRelEqns[$sq['rowdivid']] = $LEM->ParseResultCache[$sq['eqn']]['prettyprint'];
  5007. }
  5008. }
  5009. else
  5010. {
  5011. $stringToParse = htmlspecialchars_decode($sq['eqn'],ENT_QUOTES); // TODO is this needed?
  5012. $sqrel = $LEM->em->ProcessBooleanExpression($stringToParse,$qInfo['gseq'], $qInfo['qseq']);
  5013. $hasErrors = $LEM->em->HasErrors();
  5014. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5015. {
  5016. $prettyPrintSQRelEqn = $LEM->em->GetPrettyPrintString();
  5017. $prettyPrintSQRelEqns[$sq['rowdivid']] = $prettyPrintSQRelEqn;
  5018. }
  5019. $LEM->ParseResultCache[$sq['eqn']] = array(
  5020. 'result'=>$sqrel,
  5021. 'prettyprint'=>$prettyPrintSQRelEqn,
  5022. 'hasErrors'=>$hasErrors,
  5023. );
  5024. }
  5025. if ($sqrel)
  5026. {
  5027. $relevantSQs[] = $sgqa;
  5028. $_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']]=true;
  5029. }
  5030. else
  5031. {
  5032. $irrelevantSQs[] = $sgqa;
  5033. $_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']]=false;
  5034. }
  5035. }
  5036. break;
  5037. case ':': //ARRAY (Multi Flexi) 1 to 10
  5038. case ';': //ARRAY (Multi Flexi) Text
  5039. if (preg_match('/^' . $sq['rowdivid'] . '_/', $sgqa))
  5040. {
  5041. $foundSQrelevance=true;
  5042. if (isset($LEM->ParseResultCache[$sq['eqn']]))
  5043. {
  5044. $sqrel = $LEM->ParseResultCache[$sq['eqn']]['result'];
  5045. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5046. {
  5047. $prettyPrintSQRelEqns[$sq['rowdivid']] = $LEM->ParseResultCache[$sq['eqn']]['prettyprint'];
  5048. }
  5049. }
  5050. else
  5051. {
  5052. $stringToParse = htmlspecialchars_decode($sq['eqn'],ENT_QUOTES); // TODO is this needed?
  5053. $sqrel = $LEM->em->ProcessBooleanExpression($stringToParse,$qInfo['gseq'], $qInfo['qseq']);
  5054. $hasErrors = $LEM->em->HasErrors();
  5055. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5056. {
  5057. $prettyPrintSQRelEqn = $LEM->em->GetPrettyPrintString();
  5058. $prettyPrintSQRelEqns[$sq['rowdivid']] = $prettyPrintSQRelEqn;
  5059. }
  5060. $LEM->ParseResultCache[$sq['eqn']] = array(
  5061. 'result'=>$sqrel,
  5062. 'prettyprint'=>$prettyPrintSQRelEqn,
  5063. 'hasErrors'=>$hasErrors,
  5064. );
  5065. }
  5066. if ($sqrel)
  5067. {
  5068. $relevantSQs[] = $sgqa;
  5069. $_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']]=true;
  5070. }
  5071. else
  5072. {
  5073. $irrelevantSQs[] = $sgqa;
  5074. $_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']]=false;
  5075. }
  5076. }
  5077. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  5078. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  5079. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  5080. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  5081. case 'F': //ARRAY (Flexible) - Row Format
  5082. case 'M': //Multiple choice checkbox
  5083. case 'P': //Multiple choice with comments checkbox + text
  5084. // Note, for M and P, Mandatory should mean that at least one answer was picked - not that all were checked
  5085. case 'K': //MULTIPLE NUMERICAL QUESTION
  5086. case 'Q': //MULTIPLE SHORT TEXT
  5087. if ($sgqa == $sq['rowdivid'] || $sgqa == ($sq['rowdivid'] . 'comment')) // to catch case 'P'
  5088. {
  5089. $foundSQrelevance=true;
  5090. if (isset($LEM->ParseResultCache[$sq['eqn']]))
  5091. {
  5092. $sqrel = $LEM->ParseResultCache[$sq['eqn']]['result'];
  5093. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5094. {
  5095. $prettyPrintSQRelEqns[$sq['rowdivid']] = $LEM->ParseResultCache[$sq['eqn']]['prettyprint'];
  5096. }
  5097. }
  5098. else
  5099. {
  5100. $stringToParse = htmlspecialchars_decode($sq['eqn'],ENT_QUOTES); // TODO is this needed?
  5101. $sqrel = $LEM->em->ProcessBooleanExpression($stringToParse,$qInfo['gseq'], $qInfo['qseq']);
  5102. $hasErrors = $LEM->em->HasErrors();
  5103. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5104. {
  5105. $prettyPrintSQRelEqn = $LEM->em->GetPrettyPrintString();
  5106. $prettyPrintSQRelEqns[$sq['rowdivid']] = $prettyPrintSQRelEqn;
  5107. }
  5108. $LEM->ParseResultCache[$sq['eqn']] = array(
  5109. 'result'=>$sqrel,
  5110. 'prettyprint'=>$prettyPrintSQRelEqn,
  5111. 'hasErrors'=>$hasErrors,
  5112. );
  5113. }
  5114. if ($sqrel)
  5115. {
  5116. $relevantSQs[] = $sgqa;
  5117. $_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']]=true;
  5118. }
  5119. else
  5120. {
  5121. $irrelevantSQs[] = $sgqa;
  5122. $_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']]=false;
  5123. }
  5124. }
  5125. break;
  5126. case 'L': //LIST drop-down/radio-button list
  5127. if ($sgqa == ($sq['sgqa'] . 'other') && $sgqa == $sq['rowdivid']) // don't do sub-q level validition to main question, just to other option
  5128. {
  5129. $foundSQrelevance=true;
  5130. if (isset($LEM->ParseResultCache[$sq['eqn']]))
  5131. {
  5132. $sqrel = $LEM->ParseResultCache[$sq['eqn']]['result'];
  5133. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5134. {
  5135. $prettyPrintSQRelEqns[$sq['rowdivid']] = $LEM->ParseResultCache[$sq['eqn']]['prettyprint'];
  5136. }
  5137. }
  5138. else
  5139. {
  5140. $stringToParse = htmlspecialchars_decode($sq['eqn'],ENT_QUOTES); // TODO is this needed?
  5141. $sqrel = $LEM->em->ProcessBooleanExpression($stringToParse,$qInfo['gseq'], $qInfo['qseq']);
  5142. $hasErrors = $LEM->em->HasErrors();
  5143. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5144. {
  5145. $prettyPrintSQRelEqn = $LEM->em->GetPrettyPrintString();
  5146. $prettyPrintSQRelEqns[$sq['rowdivid']] = $prettyPrintSQRelEqn;
  5147. }
  5148. $LEM->ParseResultCache[$sq['eqn']] = array(
  5149. 'result'=>$sqrel,
  5150. 'prettyprint'=>$prettyPrintSQRelEqn,
  5151. 'hasErrors'=>$hasErrors,
  5152. );
  5153. }
  5154. if ($sqrel)
  5155. {
  5156. $relevantSQs[] = $sgqa;
  5157. }
  5158. else
  5159. {
  5160. $irrelevantSQs[] = $sgqa;
  5161. }
  5162. }
  5163. break;
  5164. default:
  5165. break;
  5166. }
  5167. } // end foreach($LEM->subQrelInfo) [checking array-filters]
  5168. if (!$foundSQrelevance)
  5169. {
  5170. // then this question is relevant
  5171. $relevantSQs[] = $sgqa; // TODO - check this
  5172. }
  5173. }
  5174. } // end of processing relevant question for sub-questions
  5175. if (($LEM->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX)
  5176. {
  5177. // TODO - why is array_unique needed here?
  5178. // $prettyPrintSQRelEqns = array_unique($prettyPrintSQRelEqns);
  5179. }
  5180. // These array_unique only apply to array_filter of type L (list)
  5181. $relevantSQs = array_unique($relevantSQs);
  5182. $irrelevantSQs = array_unique($irrelevantSQs);
  5183. ////////////////////////////////////////////////////////////////////
  5184. // WHICH RELEVANT, VISIBLE (SUB)-QUESTIONS HAVEN'T BEEN ANSWERED? //
  5185. ////////////////////////////////////////////////////////////////////
  5186. // check that all mandatories have been fully answered (but don't require answers for sub-questions that are irrelevant
  5187. $unansweredSQs = array(); // list of sub-questions that weren't answered
  5188. foreach ($relevantSQs as $sgqa)
  5189. {
  5190. if (($qInfo['type'] != '*') && (!isset($_SESSION[$LEM->sessid][$sgqa]) || ($_SESSION[$LEM->sessid][$sgqa] === '' || is_null($_SESSION[$LEM->sessid][$sgqa]))))
  5191. {
  5192. // then a relevant, visible, mandatory question hasn't been answered
  5193. // Equations are ignored, since set automatically
  5194. $unansweredSQs[] = $sgqa;
  5195. }
  5196. }
  5197. //////////////////////////////////////////////
  5198. // DETECT ANY VIOLATIONS OF MANDATORY RULES //
  5199. //////////////////////////////////////////////
  5200. $qmandViolation = false; // assume there is no mandatory violation until discover otherwise
  5201. $mandatoryTip = '';
  5202. if ($qrel && !$qhidden && ($qInfo['mandatory'] == 'Y'))
  5203. {
  5204. $mandatoryTip = "<strong><br /><span class='errormandatory'>".$LEM->gT('This question is mandatory').'. ';
  5205. switch ($qInfo['type'])
  5206. {
  5207. case 'M':
  5208. case 'P':
  5209. case '!': //List - dropdown
  5210. case 'L': //LIST drop-down/radio-button list
  5211. // If at least one checkbox is checked, we're OK
  5212. if (count($relevantSQs) > 0 && (count($relevantSQs) == count($unansweredSQs)))
  5213. {
  5214. $qmandViolation = true;
  5215. }
  5216. if (!($qInfo['type'] == '!' || $qInfo['type'] == 'L'))
  5217. {
  5218. $mandatoryTip .= $LEM->gT('Please check at least one item.');
  5219. }
  5220. if ($qInfo['other']=='Y')
  5221. {
  5222. $qattr = isset($LEM->qattr[$qid]) ? $LEM->qattr[$qid] : array();
  5223. if (isset($qattr['other_replace_text']) && trim($qattr['other_replace_text']) != '') {
  5224. $othertext = trim($qattr['other_replace_text']);
  5225. }
  5226. else {
  5227. $othertext = $LEM->gT('Other:');
  5228. }
  5229. $mandatoryTip .= "<br />\n".sprintf($this->gT("If you choose '%s' please also specify your choice in the accompanying text field."),$othertext);
  5230. }
  5231. break;
  5232. case 'X': // Boilerplate can never be mandatory
  5233. case '*': // Equation is auto-computed, so can't violate mandatory rules
  5234. break;
  5235. case 'A':
  5236. case 'B':
  5237. case 'C':
  5238. case 'Q':
  5239. case 'K':
  5240. case 'E':
  5241. case 'F':
  5242. case 'J':
  5243. case 'H':
  5244. case ';':
  5245. case '1':
  5246. // In general, if any relevant questions aren't answered, then it violates the mandatory rule
  5247. if (count($unansweredSQs) > 0)
  5248. {
  5249. $qmandViolation = true; // TODO - what about 'other'?
  5250. }
  5251. $mandatoryTip .= $LEM->gT('Please complete all parts').'.';
  5252. break;
  5253. case ':':
  5254. $qattr = isset($LEM->qattr[$qid]) ? $LEM->qattr[$qid] : array();
  5255. if (isset($qattr['multiflexible_checkbox']) && $qattr['multiflexible_checkbox'] == 1)
  5256. {
  5257. // Need to check whether there is at least one checked box per row
  5258. foreach ($LEM->q2subqInfo[$qid]['subqs'] as $sq)
  5259. {
  5260. if (!isset($_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']]) || $_SESSION[$LEM->sessid]['relevanceStatus'][$sq['rowdivid']])
  5261. {
  5262. $rowCount=0;
  5263. $numUnanswered=0;
  5264. foreach ($sgqas as $s)
  5265. {
  5266. if (strpos($s, $sq['rowdivid']) !== false)
  5267. {
  5268. ++$rowCount;
  5269. if (array_search($s,$unansweredSQs) !== false) {
  5270. ++$numUnanswered;
  5271. }
  5272. }
  5273. }
  5274. if ($rowCount > 0 && $rowCount == $numUnanswered)
  5275. {
  5276. $qmandViolation = true;
  5277. }
  5278. }
  5279. }
  5280. $mandatoryTip .= $LEM->gT('Please check at least one box per row').'.';
  5281. }
  5282. else
  5283. {
  5284. if (count($unansweredSQs) > 0)
  5285. {
  5286. $qmandViolation = true; // TODO - what about 'other'?
  5287. }
  5288. $mandatoryTip .= $LEM->gT('Please complete all parts').'.';
  5289. }
  5290. break;
  5291. case 'R':
  5292. if (count($unansweredSQs) > 0)
  5293. {
  5294. $qmandViolation = true; // TODO - what about 'other'?
  5295. }
  5296. $mandatoryTip .= $LEM->gT('Please rank all items').'.';
  5297. break;
  5298. case 'O': //LIST WITH COMMENT drop-down/radio-button list + textarea
  5299. $_count=0;
  5300. for ($i=0;$i<count($unansweredSQs);++$i)
  5301. {
  5302. if (preg_match("/comment$/",$unansweredSQs[$i])) {
  5303. continue;
  5304. }
  5305. ++$_count;
  5306. }
  5307. if ($_count > 0)
  5308. {
  5309. $qmandViolation = true;
  5310. }
  5311. break;
  5312. default:
  5313. if (count($unansweredSQs) > 0)
  5314. {
  5315. $qmandViolation = true;
  5316. }
  5317. break;
  5318. }
  5319. $mandatoryTip .= "</span></strong>\n";
  5320. }
  5321. /////////////////////////////////////////////////////////////
  5322. // DETECT WHETHER QUESTION SHOULD BE FLAGGED AS UNANSWERED //
  5323. /////////////////////////////////////////////////////////////
  5324. if ($qrel && !$qhidden)
  5325. {
  5326. switch ($qInfo['type'])
  5327. {
  5328. case 'M':
  5329. case 'P':
  5330. case '!': //List - dropdown
  5331. case 'L': //LIST drop-down/radio-button list
  5332. // If at least one checkbox is checked, we're OK
  5333. if (count($relevantSQs) > 0 && (count($relevantSQs) == count($unansweredSQs)))
  5334. {
  5335. $anyUnanswered = true;
  5336. }
  5337. // what about optional vs. mandatory comment and 'other' fields?
  5338. break;
  5339. default:
  5340. $anyUnanswered = (count($unansweredSQs) > 0);
  5341. break;
  5342. }
  5343. }
  5344. ///////////////////////////////////////////////
  5345. // DETECT ANY VIOLATIONS OF VALIDATION RULES //
  5346. ///////////////////////////////////////////////
  5347. $qvalid=true; // assume valid unless discover otherwise
  5348. $hasValidationEqn=false;
  5349. $prettyPrintValidEqn=''; // assume no validation eqn by default
  5350. $validationEqn='';
  5351. $validationJS=''; // assume can't generate JavaScript to validate equation
  5352. $validTip=''; // default is none
  5353. if (isset($LEM->qid2validationEqn[$qid]))
  5354. {
  5355. $hasValidationEqn=true;
  5356. if (!$qhidden) // do this even is starts irrelevant, else will never show this information.
  5357. {
  5358. $validationEqns = $LEM->qid2validationEqn[$qid]['eqn'];
  5359. $validationEqn = implode(' and ', $validationEqns);
  5360. $qvalid = $LEM->em->ProcessBooleanExpression($validationEqn,$qInfo['gseq'], $qInfo['qseq']);
  5361. $hasErrors = $LEM->em->HasErrors();
  5362. if (!$hasErrors)
  5363. {
  5364. $validationJS = $LEM->em->GetJavaScriptEquivalentOfExpression();
  5365. }
  5366. $prettyPrintValidEqn = $validationEqn;
  5367. if ((($this->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX))
  5368. {
  5369. $prettyPrintValidEqn = $LEM->em->GetPrettyPrintString();
  5370. }
  5371. $stringToParse = '';
  5372. foreach ($LEM->qid2validationEqn[$qid]['tips'] as $vclass=>$vtip)
  5373. {
  5374. $stringToParse .= "<div id='vmsg_" . $qid . '_' . $vclass . "' class='em_" . $vclass . "'>" . $vtip . "</div>\n";
  5375. }
  5376. $prettyPrintValidTip = $stringToParse;
  5377. $validTip = $LEM->ProcessString($stringToParse, $qid,NULL,false,1,1,false,false);
  5378. // TODO check for errors?
  5379. if ((($this->debugLevel & LEM_PRETTY_PRINT_ALL_SYNTAX) == LEM_PRETTY_PRINT_ALL_SYNTAX))
  5380. {
  5381. $prettyPrintValidTip = $LEM->GetLastPrettyPrintExpression();
  5382. }
  5383. $sumEqn = $LEM->qid2validationEqn[$qid]['sumEqn'];
  5384. $sumRemainingEqn = $LEM->qid2validationEqn[$qid]['sumRemainingEqn'];
  5385. // $countEqn = $LEM->qid2validationEqn[$qid]['countEqn'];
  5386. // $countRemainingEqn = $LEM->qid2validationEqn[$qid]['countRemainingEqn'];
  5387. }
  5388. else
  5389. {
  5390. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_DETAIL) == LEM_DEBUG_VALIDATION_DETAIL)
  5391. {
  5392. $prettyPrintValidEqn = 'Question is Irrelevant, so no need to further validate it';
  5393. }
  5394. }
  5395. }
  5396. if (!$qvalid)
  5397. {
  5398. $invalidSQs = $LEM->qid2code[$qid]; // TODO - currently invalidates all - should only invalidate those that truly fail validation rules.
  5399. }
  5400. /////////////////////////////////////////////////////////
  5401. // OPTIONALLY DISPLAY (DETAILED) DEBUGGING INFORMATION //
  5402. /////////////////////////////////////////////////////////
  5403. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_SUMMARY) == LEM_DEBUG_VALIDATION_SUMMARY)
  5404. {
  5405. $editlink = Yii::app()->getController()->createUrl('admin/survey/sa/view/surveyid/' . $LEM->sid . '/gid/' . $gid . '/qid/' . $qid);
  5406. $debug_qmessage .= '--[Q#' . $qInfo['qseq'] . ']'
  5407. . "[<a href='$editlink'>"
  5408. . 'QID:'. $qid . '</a>][' . $qInfo['type'] . ']: '
  5409. . ($qrel ? 'relevant' : " <span style='color:red'>irrelevant</span> ")
  5410. . ($qhidden ? " <span style='color:red'>always-hidden</span> " : ' ')
  5411. . (($qInfo['mandatory'] == 'Y')? ' mandatory' : ' ')
  5412. . (($hasValidationEqn) ? (!$qvalid ? " <span style='color:red'>(fails validation rule)</span> " : ' valid') : '')
  5413. . ($qmandViolation ? " <span style='color:red'>(missing a relevant mandatory)</span> " : ' ')
  5414. . $prettyPrintRelEqn
  5415. . "<br />\n";
  5416. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_DETAIL) == LEM_DEBUG_VALIDATION_DETAIL)
  5417. {
  5418. if ($mandatoryTip != '')
  5419. {
  5420. $debug_qmessage .= '----Mandatory Tip: ' . flattenText($mandatoryTip) . "<br />\n";
  5421. }
  5422. if ($prettyPrintValidTip != '')
  5423. {
  5424. $debug_qmessage .= '----Pretty Validation Tip: <br />' . $prettyPrintValidTip . "<br />\n";
  5425. }
  5426. if ($validTip != '')
  5427. {
  5428. $debug_qmessage .= '----Validation Tip: <br />' . $validTip . "<br />\n";
  5429. }
  5430. if ($prettyPrintValidEqn != '')
  5431. {
  5432. $debug_qmessage .= '----Validation Eqn: ' . $prettyPrintValidEqn . "<br />\n";
  5433. }
  5434. if ($validationJS != '')
  5435. {
  5436. $debug_qmessage .= '----Validation JavaScript: ' . $validationJS . "<br />\n";
  5437. }
  5438. // what are the database question codes for this question?
  5439. $subQList = '{' . implode('}, {', explode('|',$LEM->qid2code[$qid])) . '}';
  5440. // pretty-print them
  5441. $LEM->ProcessString($subQList, $qid,NULL,false,1,1,false,false);
  5442. $prettyPrintSubQList = $LEM->GetLastPrettyPrintExpression();
  5443. $debug_qmessage .= '----SubQs=> ' . $prettyPrintSubQList . "<br />\n";
  5444. if (count($prettyPrintSQRelEqns) > 0)
  5445. {
  5446. $debug_qmessage .= "----Array Filters Applied:<br />\n";
  5447. foreach ($prettyPrintSQRelEqns as $key => $value)
  5448. {
  5449. $debug_qmessage .= '------' . $key . ': ' . $value . "<br />\n";
  5450. }
  5451. $debug_qmessage .= "<br />\n";
  5452. }
  5453. if (count($relevantSQs) > 0)
  5454. {
  5455. $subQList = '{' . implode('}, {', $relevantSQs) . '}';
  5456. // pretty-print them
  5457. $LEM->ProcessString($subQList, $qid,NULL,false,1,1,false,false);
  5458. $prettyPrintSubQList = $LEM->GetLastPrettyPrintExpression();
  5459. $debug_qmessage .= '----Relevant SubQs: ' . $prettyPrintSubQList . "<br />\n";
  5460. }
  5461. if (count($irrelevantSQs) > 0)
  5462. {
  5463. $subQList = '{' . implode('}, {', $irrelevantSQs) . '}';
  5464. // pretty-print them
  5465. $LEM->ProcessString($subQList, $qid,NULL,false,1,1,false,false);
  5466. $prettyPrintSubQList = $LEM->GetLastPrettyPrintExpression();
  5467. $debug_qmessage .= '----Irrelevant SubQs: ' . $prettyPrintSubQList . "<br />\n";
  5468. }
  5469. // show which relevant subQs were not answered
  5470. if (count($unansweredSQs) > 0)
  5471. {
  5472. $subQList = '{' . implode('}, {', $unansweredSQs) . '}';
  5473. // pretty-print them
  5474. $LEM->ProcessString($subQList, $qid,NULL,false,1,1,false,false);
  5475. $prettyPrintSubQList = $LEM->GetLastPrettyPrintExpression();
  5476. $debug_qmessage .= '----Unanswered Relevant SubQs: ' . $prettyPrintSubQList . "<br />\n";
  5477. }
  5478. }
  5479. }
  5480. /////////////////////////////////////////////////////////////
  5481. // CREATE ARRAY OF VALUES THAT NEED TO BE SILENTLY UPDATED //
  5482. /////////////////////////////////////////////////////////////
  5483. $updatedValues=array();
  5484. if (!$qrel || !$grel)
  5485. {
  5486. // If not relevant, then always NULL it in the database
  5487. $sgqas = explode('|',$LEM->qid2code[$qid]);
  5488. foreach ($sgqas as $sgqa)
  5489. {
  5490. $_SESSION[$LEM->sessid][$sgqa] = NULL;
  5491. $updatedValues[$sgqa] = NULL;
  5492. $LEM->updatedValues[$sgqa] = NULL;
  5493. }
  5494. }
  5495. else if ($qInfo['type'] == '*')
  5496. {
  5497. // Process relevant equations, even if hidden, and write the result to the database
  5498. $result = flattenText($LEM->ProcessString($qInfo['eqn'], $qInfo['qid'],NULL,false,1,1,false,false));
  5499. $sgqa = $LEM->qid2code[$qid]; // there will be only one, since Equation
  5500. // Store the result of the Equation in the SESSION
  5501. $_SESSION[$LEM->sessid][$sgqa] = $result;
  5502. $_update = array(
  5503. 'type'=>'*',
  5504. 'value'=>$result,
  5505. );
  5506. $updatedValues[$sgqa] = $_update;
  5507. $LEM->updatedValues[$sgqa] = $_update;
  5508. if (($LEM->debugLevel & LEM_DEBUG_VALIDATION_DETAIL) == LEM_DEBUG_VALIDATION_DETAIL)
  5509. {
  5510. $prettyPrintEqn = $LEM->em->GetPrettyPrintString();
  5511. $debug_qmessage .= '** Process Hidden but Relevant Equation [' . $sgqa . '](' . $prettyPrintEqn . ') => ' . $result . "<br />\n";
  5512. }
  5513. }
  5514. foreach ($irrelevantSQs as $sq)
  5515. {
  5516. // NULL irrelevant sub-questions
  5517. $_SESSION[$LEM->sessid][$sq] = NULL;
  5518. $updatedValues[$sq] = NULL;
  5519. $LEM->updatedValues[$sq]= NULL;
  5520. }
  5521. // Regardless of whether relevant or hidden, if there is a default value and $_SESSION[$LEM->sessid][$sgqa] is NULL, then use the default value in $_SESSION, but don't write to database
  5522. // Also, set this AFTER testing relevance
  5523. $sgqas = explode('|',$LEM->qid2code[$qid]);
  5524. foreach ($sgqas as $sgqa)
  5525. {
  5526. if (!is_null($LEM->knownVars[$sgqa]['default']) && !isset($_SESSION[$LEM->sessid][$sgqa])) {
  5527. // add support for replacements
  5528. $defaultVal = $LEM->ProcessString($LEM->knownVars[$sgqa]['default'], NULL, NULL, false, 1, 1, false, false, true);
  5529. $_SESSION[$LEM->sessid][$sgqa] = $defaultVal;
  5530. }
  5531. }
  5532. //////////////////////////////////////////////////////////////////////////
  5533. // STORE METADATA NEEDED FOR SUBSEQUENT PROCESSING AND DISPLAY PURPOSES //
  5534. //////////////////////////////////////////////////////////////////////////
  5535. $qStatus = array(
  5536. 'info' => $qInfo, // collect all questions within the group - includes mandatory and always-hiddden status
  5537. 'relevant' => $qrel,
  5538. 'hidden' => $qInfo['hidden'],
  5539. 'relEqn' => $prettyPrintRelEqn,
  5540. 'sgqa' => $LEM->qid2code[$qid],
  5541. 'unansweredSQs' => implode('|',$unansweredSQs),
  5542. 'valid' => $qvalid,
  5543. 'validEqn' => $validationEqn,
  5544. 'prettyValidEqn' => $prettyPrintValidEqn,
  5545. 'validTip' => $validTip,
  5546. 'prettyValidTip' => $prettyPrintValidTip,
  5547. 'validJS' => $validationJS,
  5548. 'invalidSQs' => (isset($invalidSQs) ? $invalidSQs : ''),
  5549. 'relevantSQs' => implode('|',$relevantSQs),
  5550. 'irrelevantSQs' => implode('|',$irrelevantSQs),
  5551. 'subQrelEqn' => implode('<br />',$prettyPrintSQRelEqns),
  5552. 'mandViolation' => $qmandViolation,
  5553. 'anyUnanswered' => $anyUnanswered,
  5554. 'mandTip' => $mandatoryTip,
  5555. 'message' => $debug_qmessage,
  5556. 'updatedValues' => $updatedValues,
  5557. 'sumEqn' => (isset($sumEqn) ? $sumEqn : ''),
  5558. 'sumRemainingEqn' => (isset($sumRemainingEqn) ? $sumRemainingEqn : ''),
  5559. // 'countEqn' => (isset($countEqn) ? $countEqn : ''),
  5560. // 'countRemainingEqn' => (isset($countRemainingEqn) ? $countRemainingEqn : ''),
  5561. );
  5562. $LEM->currentQset[$qid] = $qStatus;
  5563. ////////////////////////////////////////////////////////
  5564. // STORE METADATA NEEDED TO GENERATE NAVIGATION INDEX //
  5565. ////////////////////////////////////////////////////////
  5566. $groupSeq = $qInfo['gseq'];
  5567. $LEM->indexQseq[$questionSeq] = array(
  5568. 'qid' => $qInfo['qid'],
  5569. 'qtext' => $qInfo['qtext'],
  5570. 'qcode' => $qInfo['code'],
  5571. 'qhelp' => $qInfo['help'],
  5572. 'anyUnanswered' => $anyUnanswered,
  5573. 'anyErrors' => (($qmandViolation || !$qvalid) ? true : false),
  5574. 'show' => (($qrel && !$qInfo['hidden']) ? true : false),
  5575. 'gseq' => $groupSeq,
  5576. 'gtext' => $LEM->gseq2info[$groupSeq]['description'],
  5577. 'gname' => $LEM->gseq2info[$groupSeq]['group_name'],
  5578. 'gid' => $LEM->gseq2info[$groupSeq]['gid'],
  5579. 'mandViolation' => $qmandViolation,
  5580. 'valid' => $qvalid,
  5581. );
  5582. $_SESSION[$LEM->sessid]['relevanceStatus'][$qid] = $qrel;
  5583. return $qStatus;
  5584. }
  5585. static function GetQuestionStatus($qid)
  5586. {
  5587. $LEM =& LimeExpressionManager::singleton();
  5588. if (isset($LEM->currentQset[$qid]))
  5589. {
  5590. return $LEM->currentQset[$qid];
  5591. }
  5592. return NULL;
  5593. }
  5594. /**
  5595. * Get array of info needed to display the Group Index
  5596. * @return <type>
  5597. */
  5598. static function GetGroupIndexInfo($gseq=NULL)
  5599. {
  5600. $LEM =& LimeExpressionManager::singleton();
  5601. if (is_null($gseq)) {
  5602. return $LEM->indexGseq;
  5603. }
  5604. else {
  5605. return $LEM->indexGseq[$gseq];
  5606. }
  5607. }
  5608. /**
  5609. * Translate GID to 0-index Group Sequence number
  5610. * @param <type> $gid
  5611. * @return <type>
  5612. */
  5613. static function GetGroupSeq($gid)
  5614. {
  5615. $LEM =& LimeExpressionManager::singleton();
  5616. return (isset($LEM->groupId2groupSeq[$gid]) ? $LEM->groupId2groupSeq[$gid] : -1);
  5617. }
  5618. /**
  5619. * Get question sequence number from QID
  5620. * @param <type> $qid
  5621. * @return <type>
  5622. */
  5623. static function GetQuestionSeq($qid)
  5624. {
  5625. $LEM =& LimeExpressionManager::singleton();
  5626. return (isset($LEM->questionId2questionSeq[$qid]) ? $LEM->questionId2questionSeq[$qid] : -1);
  5627. }
  5628. /**
  5629. * Get array of info needed to display the Question Index
  5630. * @return <type>
  5631. */
  5632. static function GetQuestionIndexInfo()
  5633. {
  5634. $LEM =& LimeExpressionManager::singleton();
  5635. return $LEM->indexQseq;
  5636. }
  5637. /**
  5638. * Return entries needed to build the navigation index
  5639. * @param <type> $step - if specified, return a single value, otherwise return entire array
  5640. * @return <type> - will be either question or group-level, depending upon $surveyMode
  5641. */
  5642. static function GetStepIndexInfo($step=NULL)
  5643. {
  5644. $LEM =& LimeExpressionManager::singleton();
  5645. switch ($LEM->surveyMode)
  5646. {
  5647. case 'survey':
  5648. return $LEM->lastMoveResult;
  5649. break;
  5650. case 'group':
  5651. if (is_null($step)) {
  5652. return $LEM->indexGseq;
  5653. }
  5654. return $LEM->indexGseq[$step];
  5655. break;
  5656. case 'question':
  5657. if (is_null($step)) {
  5658. return $LEM->indexQseq;
  5659. }
  5660. return $LEM->indexQseq[$step];
  5661. break;
  5662. }
  5663. }
  5664. /**
  5665. * This should be called each time a new group is started, whether on same or different pages. Sets/Clears needed internal parameters.
  5666. * @param <type> $gseq - the group sequence
  5667. * @param <type> $anonymized - whether anonymized
  5668. * @param <type> $surveyid - the surveyId
  5669. * @param <type> $forceRefresh - whether to force refresh of setting variable and token mappings (should be done rarely)
  5670. */
  5671. static function StartProcessingGroup($gseq=NULL,$anonymized=false,$surveyid=NULL,$forceRefresh=false)
  5672. {
  5673. $LEM =& LimeExpressionManager::singleton();
  5674. $LEM->em->StartProcessingGroup(
  5675. isset($surveyid) ? $surveyid : NULL,
  5676. '',
  5677. isset($LEM->surveyOptions['hyperlinkSyntaxHighlighting']) ? $LEM->surveyOptions['hyperlinkSyntaxHighlighting'] : false
  5678. );
  5679. $LEM->groupRelevanceInfo = array();
  5680. if (!is_null($gseq))
  5681. {
  5682. $LEM->currentGroupSeq = $gseq;
  5683. if (!is_null($surveyid))
  5684. {
  5685. $LEM->setVariableAndTokenMappingsForExpressionManager($surveyid,$forceRefresh,$anonymized,$LEM->allOnOnePage);
  5686. if ($gseq > $LEM->maxGroupSeq) {
  5687. $LEM->maxGroupSeq = $gseq;
  5688. }
  5689. if (!$LEM->allOnOnePage || ($LEM->allOnOnePage && !$LEM->processedRelevance)) {
  5690. $LEM->ProcessAllNeededRelevance(); // TODO - what if this is called using Survey or Data Entry format?
  5691. $LEM->_CreateSubQLevelRelevanceAndValidationEqns();
  5692. $LEM->processedRelevance=true;
  5693. }
  5694. }
  5695. }
  5696. }
  5697. /**
  5698. * Should be called after each group finishes
  5699. */
  5700. static function FinishProcessingGroup($skipReprocessing=false)
  5701. {
  5702. // $now = microtime(true);
  5703. $LEM =& LimeExpressionManager::singleton();
  5704. if ($skipReprocessing && $LEM->surveyMode != 'survey')
  5705. {
  5706. $LEM->pageTailorInfo=array();
  5707. $LEM->pageRelevanceInfo=array();
  5708. }
  5709. $LEM->pageTailorInfo[] = $LEM->em->GetCurrentSubstitutionInfo();
  5710. $LEM->pageRelevanceInfo[] = $LEM->groupRelevanceInfo;
  5711. // $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  5712. }
  5713. /**
  5714. * Returns an array of string parts, splitting out expressions
  5715. * @param type $src
  5716. * @return type
  5717. */
  5718. static function SplitStringOnExpressions($src)
  5719. {
  5720. $LEM =& LimeExpressionManager::singleton();
  5721. return $LEM->em->asSplitStringOnExpressions($src);
  5722. }
  5723. /**
  5724. * Return a formatted table showing how much time each part of EM consumed
  5725. * @return <type>
  5726. */
  5727. static function GetDebugTimingMessage()
  5728. {
  5729. $LEM =& LimeExpressionManager::singleton();
  5730. return $LEM->debugTimingMsg;
  5731. }
  5732. /**
  5733. * Should be called at end of each page
  5734. */
  5735. static function FinishProcessingPage()
  5736. {
  5737. $LEM =& LimeExpressionManager::singleton();
  5738. $totalTime = 0.;
  5739. if ((($LEM->debugLevel & LEM_DEBUG_TIMING) == LEM_DEBUG_TIMING) && count($LEM->runtimeTimings)>0) {
  5740. $LEM->debugTimingMsg='';
  5741. foreach($LEM->runtimeTimings as $unit) {
  5742. $totalTime += $unit[1];
  5743. }
  5744. $LEM->debugTimingMsg .= "<table border='1'><tr><td colspan=2><b>Total time attributable to EM = " . $totalTime . "</b></td></tr>\n";
  5745. foreach ($LEM->runtimeTimings as $t)
  5746. {
  5747. $LEM->debugTimingMsg .= "<tr><td>" . $t[0] . "</td><td>" . $t[1] . "</td></tr>\n";
  5748. }
  5749. $LEM->debugTimingMsg .= "</table>\n";
  5750. }
  5751. $LEM->runtimeTimings = array(); // reset them
  5752. $LEM->initialized=false; // so detect calls after done
  5753. $LEM->ParseResultCache=array(); // don't need to persist it in session
  5754. $_SESSION['LEMsingleton']=serialize($LEM);
  5755. }
  5756. /*
  5757. * Generate JavaScript needed to do dynamic relevance and tailoring
  5758. * Also create list of variables that need to be declared
  5759. */
  5760. static function GetRelevanceAndTailoringJavaScript()
  5761. {
  5762. $now = microtime(true);
  5763. $LEM =& LimeExpressionManager::singleton();
  5764. $jsParts=array();
  5765. $allJsVarsUsed=array();
  5766. $rowdividList=array(); // list of subquestions needing relevance entries
  5767. $jsParts[] = '<script type="text/javascript" src="'.Yii::app()->getConfig('generalscripts').'expressions/em_javascript.js"></script>';
  5768. $jsParts[] = "\n<script type='text/javascript'>\n<!--\n";
  5769. $jsParts[] = "var LEMmode='" . $LEM->surveyMode . "';\n";
  5770. if ($LEM->surveyMode == 'group')
  5771. {
  5772. $jsParts[] = "var LEMgseq=" . $LEM->currentGroupSeq . ";\n";
  5773. }
  5774. if ($LEM->surveyMode == 'question' && isset($LEM->currentQID))
  5775. {
  5776. $jsParts[] = "var LEMqid=" . $LEM->currentQID . ";\n"; // current group num so can compute isOnCurrentPage
  5777. }
  5778. $jsParts[] = "function ExprMgr_process_relevance_and_tailoring(evt_type,sgqa,type){\n";
  5779. $jsParts[] = "if (typeof LEM_initialized == 'undefined') {\nLEM_initialized=true;\nLEMsetTabIndexes();\n}\n";
  5780. $jsParts[] = "if (evt_type == 'onchange' && (typeof last_sgqa !== 'undefined' && sgqa==last_sgqa) && (typeof last_evt_type !== 'undefined' && last_evt_type == 'TAB' && type != 'checkbox')) {\n";
  5781. $jsParts[] = " last_evt_type='onchange';\n";
  5782. $jsParts[] = " last_sgqa=sgqa;\n";
  5783. $jsParts[] = " return;\n";
  5784. $jsParts[] = "}\n";
  5785. $jsParts[] = "last_evt_type = evt_type;\n";
  5786. $jsParts[] = "last_sgqa=sgqa;\n";
  5787. // flatten relevance array, keeping proper order
  5788. $pageRelevanceInfo=array();
  5789. $qidList = array(); // list of questions used in relevance and tailoring
  5790. $gseqList = array(); // list of gseqs on this page
  5791. $gseq_qidList = array(); // list of qids using relevance/tailoring within each group
  5792. if (is_array($LEM->pageRelevanceInfo))
  5793. {
  5794. foreach($LEM->pageRelevanceInfo as $prel)
  5795. {
  5796. foreach($prel as $rel)
  5797. {
  5798. $pageRelevanceInfo[] = $rel;
  5799. }
  5800. }
  5801. }
  5802. $valEqns = array();
  5803. $relEqns = array();
  5804. $relChangeVars = array();
  5805. $dynamicQinG = array(); // array of questions, per group, that might affect group-level visibility in all-in-one mode
  5806. $GalwaysRelevant = array(); // checks whether a group is always relevant (e.g. has at least one question that is always shown)
  5807. if (is_array($pageRelevanceInfo))
  5808. {
  5809. foreach ($pageRelevanceInfo as $arg)
  5810. {
  5811. if (!$LEM->allOnOnePage && $LEM->currentGroupSeq != $arg['gseq']) {
  5812. continue;
  5813. }
  5814. $gseqList[$arg['gseq']] = $arg['gseq']; // so keep them in order
  5815. // First check if there is any tailoring and construct the tailoring JavaScript if needed
  5816. $tailorParts = array();
  5817. $relParts = array(); // relevance equation
  5818. $valParts = array(); // validation
  5819. $relJsVarsUsed = array(); // vars used in relevance and tailoring
  5820. $valJsVarsUsed = array(); // vars used in validations
  5821. foreach ($LEM->pageTailorInfo as $tailor)
  5822. {
  5823. if (is_array($tailor))
  5824. {
  5825. foreach ($tailor as $sub)
  5826. {
  5827. if ($sub['questionNum'] == $arg['qid'])
  5828. {
  5829. $tailorParts[] = $sub['js'];
  5830. $vars = explode('|',$sub['vars']);
  5831. if (is_array($vars))
  5832. {
  5833. $allJsVarsUsed = array_merge($allJsVarsUsed,$vars);
  5834. $relJsVarsUsed = array_merge($relJsVarsUsed,$vars);
  5835. }
  5836. }
  5837. }
  5838. }
  5839. }
  5840. // Now check whether there is sub-question relevance to perform for this question
  5841. $subqParts = array();
  5842. if (isset($LEM->subQrelInfo[$arg['qid']]))
  5843. {
  5844. foreach ($LEM->subQrelInfo[$arg['qid']] as $subq)
  5845. {
  5846. $subqParts[$subq['rowdivid']] = $subq;
  5847. }
  5848. }
  5849. $qidList[$arg['qid']] = $arg['qid'];
  5850. if (!isset($gseq_qidList[$arg['gseq']]))
  5851. {
  5852. $gseq_qidList[$arg['gseq']] = array();
  5853. }
  5854. $gseq_qidList[$arg['gseq']][$arg['qid']] = '0'; // means the qid is within this gseq, but may not have a relevance equation
  5855. // Now check whether any sub-question validation needs to be performed
  5856. $subqValidations = array();
  5857. $validationEqns = array();
  5858. if (isset($LEM->qid2validationEqn[$arg['qid']]))
  5859. {
  5860. if (isset($LEM->qid2validationEqn[$arg['qid']]['subqValidEqns']))
  5861. {
  5862. $_veqs = $LEM->qid2validationEqn[$arg['qid']]['subqValidEqns'];
  5863. foreach($_veqs as $_veq)
  5864. {
  5865. // generate JavaScript for each - tests whether invalid.
  5866. if (strlen(trim($_veq['subqValidEqn'])) == 0) {
  5867. continue;
  5868. }
  5869. $subqValidations[] = array(
  5870. 'subqValidEqn' => $_veq['subqValidEqn'],
  5871. 'subqValidSelector' => $_veq['subqValidSelector'],
  5872. );
  5873. }
  5874. }
  5875. $validationEqns = $LEM->qid2validationEqn[$arg['qid']]['eqn'];
  5876. }
  5877. // Process relevance for question $arg['qid'];
  5878. $relevance = $arg['relevancejs'];
  5879. $relChangeVars[] = " relChange" . $arg['qid'] . "=false;\n"; // detect change in relevance status
  5880. if (($relevance == '' || $relevance == '1' || ($arg['result'] == true && $arg['numJsVars']==0)) && count($tailorParts) == 0 && count($subqParts) == 0 && count($subqValidations) == 0 && count($validationEqns) == 0)
  5881. {
  5882. // Only show constitutively true relevances if there is tailoring that should be done.
  5883. $relParts[] = "$('#relevance" . $arg['qid'] . "').val('1'); // always true\n";
  5884. $GalwaysRelevant[$arg['gseq']] = true;
  5885. continue;
  5886. }
  5887. $relevance = ($relevance == '' || ($arg['result'] == true && $arg['numJsVars']==0)) ? '1' : $relevance;
  5888. $relParts[] = "\nif (" . $relevance . ")\n{\n";
  5889. ////////////////////////////////////////////////////////////////////////
  5890. // DO ALL ARRAY FILTERING FIRST - MAY AFFECT VALIDATION AND TAILORING //
  5891. ////////////////////////////////////////////////////////////////////////
  5892. // Do all sub-question filtering (e..g array_filter)
  5893. /**
  5894. * $afHide - if true, then use jQuery.show(). If false, then disable/enable the row
  5895. */
  5896. $afHide = (isset($LEM->qattr[$arg['qid']]['array_filter_style']) ? ($LEM->qattr[$arg['qid']]['array_filter_style'] == '0') : true);
  5897. $inputSelector = (($arg['type'] == 'R') ? '' : ' :input:not(:hidden)');
  5898. foreach ($subqParts as $sq)
  5899. {
  5900. $rowdividList[$sq['rowdivid']] = $sq['result'];
  5901. $relParts[] = " if ( " . $sq['relevancejs'] . " ) {\n";
  5902. if ($afHide)
  5903. {
  5904. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "').show();\n";
  5905. }
  5906. else
  5907. {
  5908. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').removeAttr('disabled');\n";
  5909. }
  5910. if ($sq['isExclusiveJS'] != '')
  5911. {
  5912. $relParts[] = " if ( " . $sq['isExclusiveJS'] . " ) {\n";
  5913. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').attr('disabled','disabled');\n";
  5914. $relParts[] = " }\n";
  5915. $relParts[] = " else {\n";
  5916. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').removeAttr('disabled');\n";
  5917. $relParts[] = " }\n";
  5918. }
  5919. $relParts[] = " relChange" . $arg['qid'] . "=true;\n";
  5920. $relParts[] = " $('#relevance" . $sq['rowdivid'] . "').val('1');\n";
  5921. $relParts[] = " }\n else {\n";
  5922. if ($sq['isExclusiveJS'] != '')
  5923. {
  5924. if ($sq['irrelevantAndExclusiveJS'] != '')
  5925. {
  5926. $relParts[] = " if ( " . $sq['irrelevantAndExclusiveJS'] . " ) {\n";
  5927. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').attr('disabled','disabled');\n";
  5928. $relParts[] = " }\n";
  5929. $relParts[] = " else {\n";
  5930. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').removeAttr('disabled');\n";
  5931. if ($afHide)
  5932. {
  5933. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "').hide();\n";
  5934. }
  5935. else
  5936. {
  5937. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').attr('disabled','disabled');\n";
  5938. }
  5939. $relParts[] = " }\n";
  5940. }
  5941. else
  5942. {
  5943. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').attr('disabled','disabled');\n";
  5944. }
  5945. }
  5946. else
  5947. {
  5948. if ($afHide)
  5949. {
  5950. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "').hide();\n";
  5951. }
  5952. else
  5953. {
  5954. $relParts[] = " $('#javatbd" . $sq['rowdivid'] . "$inputSelector').attr('disabled','disabled');\n";
  5955. }
  5956. }
  5957. $relParts[] = " relChange" . $arg['qid'] . "=true;\n";
  5958. $relParts[] = " $('#relevance" . $sq['rowdivid'] . "').val('');\n";
  5959. switch ($sq['qtype'])
  5960. {
  5961. case 'L': //LIST drop-down/radio-button list
  5962. $listItem = substr($sq['rowdivid'],strlen($sq['sgqa'])); // gets the part of the rowdiv id past the end of the sgqa code.
  5963. $relParts[] = " if (($('#java" . $sq['sgqa'] ."').val() == '" . $listItem . "')";
  5964. if ($listItem == 'other') {
  5965. $relParts[] = " || ($('#java" . $sq['sgqa'] ."').val() == '-oth-')";
  5966. }
  5967. $relParts[] = "){\n";
  5968. $relParts[] = " $('#java" . $sq['sgqa'] . "').val('');\n";
  5969. $relParts[] = " $('#answer" . $sq['sgqa'] . "NANS').attr('checked',true);\n";
  5970. $relParts[] = " }\n";
  5971. break;
  5972. default:
  5973. break;
  5974. }
  5975. $relParts[] = " }\n";
  5976. $sqvars = explode('|',$sq['relevanceVars']);
  5977. if (is_array($sqvars))
  5978. {
  5979. $allJsVarsUsed = array_merge($allJsVarsUsed,$sqvars);
  5980. $relJsVarsUsed = array_merge($relJsVarsUsed,$sqvars);
  5981. }
  5982. }
  5983. // Do all tailoring
  5984. $relParts[] = implode("\n",$tailorParts);
  5985. // Do custom validation
  5986. foreach ($subqValidations as $_veq)
  5987. {
  5988. if ($_veq['subqValidSelector'] == '') {
  5989. continue;
  5990. }
  5991. $isValid = $LEM->em->ProcessBooleanExpression($_veq['subqValidEqn'],$arg['gseq'],$LEM->questionId2questionSeq[$arg['qid']]);
  5992. $_sqValidVars = $LEM->em->GetJSVarsUsed();
  5993. $allJsVarsUsed = array_merge($allJsVarsUsed,$_sqValidVars);
  5994. $valJsVarsUsed = array_merge($valJsVarsUsed,$_sqValidVars);
  5995. $validationJS = $LEM->em->GetJavaScriptEquivalentOfExpression();
  5996. $valParts[] = "\n if(" . $validationJS . "){\n";
  5997. $valParts[] = " $('#" . $_veq['subqValidSelector'] . "').addClass('em_sq_validation').removeClass('error').addClass('good');;\n";
  5998. $valParts[] = " }\n else {\n";
  5999. $valParts[] = " $('#" . $_veq['subqValidSelector'] . "').addClass('em_sq_validation').removeClass('good').addClass('error');\n";
  6000. $valParts[] = " }\n";
  6001. }
  6002. // Set color-coding for validation equations
  6003. if (count($validationEqns) > 0) {
  6004. $_hasSumRange=false;
  6005. $_hasOtherValidation=false;
  6006. $_hasOther2Validation=false;
  6007. $valParts[] = " isValidSum" . $arg['qid'] . "=true;\n"; // assume valid until proven otherwise
  6008. $valParts[] = " isValidOther" . $arg['qid'] . "=true;\n"; // assume valid until proven otherwise
  6009. $valParts[] = " isValidOtherComment" . $arg['qid'] . "=true;\n"; // assume valid until proven otherwise
  6010. foreach ($validationEqns as $vclass=>$validationEqn)
  6011. {
  6012. if ($validationEqn == '') {
  6013. continue;
  6014. }
  6015. if ($vclass == 'sum_range')
  6016. {
  6017. $_hasSumRange = true;
  6018. }
  6019. else if ($vclass == 'other_comment_mandatory')
  6020. {
  6021. $_hasOther2Validation = true;
  6022. }
  6023. else
  6024. {
  6025. $_hasOtherValidation = true;
  6026. }
  6027. $_isValid = $LEM->em->ProcessBooleanExpression($validationEqn,$arg['gseq'],$LEM->questionId2questionSeq[$arg['qid']]);
  6028. $_vars = $LEM->em->GetJSVarsUsed();
  6029. $allJsVarsUsed = array_merge($allJsVarsUsed,$_vars);
  6030. $valJsVarsUsed = array_merge($valJsVarsUsed,$_vars);
  6031. $_validationJS = $LEM->em->GetJavaScriptEquivalentOfExpression();
  6032. $valParts[] = "\n if(" . $_validationJS . "){\n";
  6033. $valParts[] = " $('#vmsg_" . $arg['qid'] . '_' . $vclass . "').removeClass('error').addClass('good');\n";
  6034. $valParts[] = " }\n else {\n";
  6035. $valParts[] = " $('#vmsg_" . $arg['qid'] . '_' . $vclass ."').removeClass('good').addClass('error');\n";
  6036. switch ($vclass)
  6037. {
  6038. case 'sum_range':
  6039. $valParts[] = " isValidSum" . $arg['qid'] . "=false;\n";
  6040. break;
  6041. case 'other_comment_mandatory':
  6042. $valParts[] = " isValidOtherComment" . $arg['qid'] . "=false;\n";
  6043. break;
  6044. // case 'num_answers':
  6045. // case 'value_range':
  6046. // case 'sq_fn_validation':
  6047. // case 'q_fn_validation':
  6048. // case 'regex_validation':
  6049. default:
  6050. $valParts[] = " isValidOther" . $arg['qid'] . "=false;\n";
  6051. break;
  6052. }
  6053. $valParts[] = " }\n";
  6054. }
  6055. $valParts[] = "\n if(isValidSum" . $arg['qid'] . "){\n";
  6056. $valParts[] = " $('#totalvalue_" . $arg['qid'] . "').removeClass('error').addClass('good');\n";
  6057. $valParts[] = " }\n else {\n";
  6058. $valParts[] = " $('#totalvalue_" . $arg['qid'] . "').removeClass('good').addClass('error');\n";
  6059. $valParts[] = " }\n";
  6060. // color-code single-entry fields as needed
  6061. switch ($arg['type'])
  6062. {
  6063. case 'N':
  6064. case 'S':
  6065. case 'T':
  6066. case 'U':
  6067. $valParts[] = "\n if(isValidOther" . $arg['qid'] . "){\n";
  6068. $valParts[] = " $('#question" . $arg['qid'] . " :input').addClass('em_sq_validation').removeClass('error').addClass('good');\n";
  6069. $valParts[] = " }\n else {\n";
  6070. $valParts[] = " $('#question" . $arg['qid'] . " :input').addClass('em_sq_validation').removeClass('good').addClass('error');\n";
  6071. $valParts[] = " }\n";
  6072. break;
  6073. default:
  6074. break;
  6075. }
  6076. // color-code mandatory other comment fields
  6077. switch ($arg['type'])
  6078. {
  6079. case '!':
  6080. case 'L':
  6081. case 'P':
  6082. switch ($arg['type'])
  6083. {
  6084. case '!':
  6085. $othervar = 'othertext' . substr($arg['jsResultVar'],4,-5);
  6086. break;
  6087. case 'L':
  6088. $othervar = 'answer' . substr($arg['jsResultVar'],4) . 'text';
  6089. break;
  6090. case 'P':
  6091. $othervar = 'answer' . substr($arg['jsResultVar'],4);
  6092. break;
  6093. }
  6094. $valParts[] = "\n if(isValidOtherComment" . $arg['qid'] . "){\n";
  6095. $valParts[] = " $('#" . $othervar . "').addClass('em_sq_validation').removeClass('error').addClass('good');\n";
  6096. $valParts[] = " }\n else {\n";
  6097. $valParts[] = " $('#" . $othervar . "').addClass('em_sq_validation').removeClass('good').addClass('error');\n";
  6098. $valParts[] = " }\n";
  6099. break;
  6100. default:
  6101. break;
  6102. }
  6103. }
  6104. if (count($valParts) > 0)
  6105. {
  6106. $valJsVarsUsed = array_unique($valJsVarsUsed);
  6107. $qvalJS = "function LEMval" . $arg['qid'] . "(sgqa){\n";
  6108. // $qvalJS .= " var UsesVars = ' " . implode(' ', $valJsVarsUsed) . " ';\n";
  6109. // $qvalJS .= " if (typeof sgqa !== 'undefined' && !LEMregexMatch('/ java' + sgqa + ' /', UsesVars)) {\n return;\n }\n";
  6110. $qvalJS .= implode("",$valParts);
  6111. $qvalJS .= "}\n";
  6112. $valEqns[] = $qvalJS;
  6113. $relParts[] = " LEMval" . $arg['qid'] . "(sgqa);\n";
  6114. }
  6115. if ($arg['hidden']) {
  6116. $relParts[] = " // This question should always be hidden\n";
  6117. $relParts[] = " $('#question" . $arg['qid'] . "').hide();\n";
  6118. }
  6119. else {
  6120. if (!($relevance == '' || $relevance == '1' || ($arg['result'] == true && $arg['numJsVars']==0)))
  6121. {
  6122. // In such cases, PHP will make the question visible by default. By not forcing a re-show(), template.js can hide questions with impunity
  6123. $relParts[] = " $('#question" . $arg['qid'] . "').show();\n";
  6124. if ($arg['type'] == 'S')
  6125. {
  6126. $relParts[] = " if($('#question" . $arg['qid'] . " div[id^=\"gmap_canvas\"]').length > 0)\n";
  6127. $relParts[] = " {\n";
  6128. $relParts[] = " resetMap(" . $arg['qid'] . ");\n";
  6129. $relParts[] = " }\n";
  6130. }
  6131. }
  6132. }
  6133. // If it is an equation, and relevance is true, then write the value from the question to the answer field storing the result
  6134. if ($arg['type'] == '*')
  6135. {
  6136. $relParts[] = " // Write value from the question into the answer field\n";
  6137. $jsResultVar = $LEM->em->GetJsVarFor($arg['jsResultVar']);
  6138. // Note, this will destroy embedded HTML in the equation (e.g. if it is a report)
  6139. // Should be possible to use jQuery to remove just the LEMtailoring span, but not easy since done (the following doesn't work)
  6140. // _tmpval = $('#question801 .em_equation').clone()
  6141. // $(_tmpval).find('[id^=LEMtailor]').each(function(){ $(this).replaceWith(function(){ $(this).contents; }); })
  6142. $relParts[] = " $('#" . substr($jsResultVar,1,-1) . "').val($.trim(LEMstrip_tags($('#question" . $arg['qid'] . " .em_equation').html())));\n";
  6143. }
  6144. $relParts[] = " relChange" . $arg['qid'] . "=true;\n"; // any change to this value should trigger a propagation of changess
  6145. $relParts[] = " $('#relevance" . $arg['qid'] . "').val('1');\n";
  6146. $relParts[] = "}\n";
  6147. if (!($relevance == '' || $relevance == '1' || ($arg['result'] == true && $arg['numJsVars']==0)))
  6148. {
  6149. if (!isset($dynamicQinG[$arg['gseq']]))
  6150. {
  6151. $dynamicQinG[$arg['gseq']] = array();
  6152. }
  6153. $dynamicQinG[$arg['gseq']][$arg['qid']]=true;
  6154. $relParts[] = "else {\n";
  6155. $relParts[] = " $('#question" . $arg['qid'] . "').hide();\n";
  6156. $relParts[] = " if ($('#relevance" . $arg['qid'] . "').val()=='1') { relChange" . $arg['qid'] . "=true; }\n"; // only propagate changes if changing from relevant to irrelevant
  6157. $relParts[] = " $('#relevance" . $arg['qid'] . "').val('0');\n";
  6158. $relParts[] = "}\n";
  6159. }
  6160. $vars = explode('|',$arg['relevanceVars']);
  6161. if (is_array($vars))
  6162. {
  6163. $allJsVarsUsed = array_merge($allJsVarsUsed,$vars);
  6164. $relJsVarsUsed = array_merge($relJsVarsUsed,$vars);
  6165. }
  6166. $relJsVarsUsed = array_merge($relJsVarsUsed,$valJsVarsUsed);
  6167. $relJsVarsUsed = array_unique($relJsVarsUsed);
  6168. $qrelQIDs = array();
  6169. $qrelgseqs = array(); // so that any group-level change is also propagated
  6170. foreach ($relJsVarsUsed as $jsVar)
  6171. {
  6172. if ($jsVar != '' && isset($LEM->knownVars[substr($jsVar,4)]['qid']))
  6173. {
  6174. $knownVar = $LEM->knownVars[substr($jsVar,4)];
  6175. if ($LEM->surveyMode=='group' && $knownVar['gseq'] != $LEM->currentGroupSeq) {
  6176. continue; // don't make dependent upon off-page variables
  6177. }
  6178. $_qid = $knownVar['qid'];
  6179. if ($_qid == $arg['qid']) {
  6180. continue; // don't make dependent upon itself
  6181. }
  6182. $qrelQIDs[] = 'relChange' . $_qid;
  6183. $qrelgseqs[] = 'relChangeG' . $knownVar['gseq'];
  6184. }
  6185. }
  6186. $qrelgseqs[] = 'relChangeG' . $arg['gseq']; // so if current group changes visibility, re-tailor it.
  6187. $qrelQIDs = array_unique($qrelQIDs);
  6188. $qrelgseqs = array_unique($qrelgseqs);
  6189. if ($LEM->surveyMode=='question')
  6190. {
  6191. $qrelQIDs=array(); // in question-by-questin mode, should never test for dependencies on self or other questions.
  6192. $qrelgseqs=array();
  6193. }
  6194. $qrelJS = "function LEMrel" . $arg['qid'] . "(sgqa){\n";
  6195. $qrelJS .= " var UsesVars = ' " . implode(' ', $relJsVarsUsed) . " ';\n";
  6196. if (count($qrelQIDs) > 0)
  6197. {
  6198. $qrelJS .= " if(" . implode(' || ', $qrelQIDs) . "){\n ;\n }\n else";
  6199. }
  6200. if (count($qrelgseqs) > 0)
  6201. {
  6202. $qrelJS .= " if(" . implode(' || ', $qrelgseqs) . "){\n ;\n }\n else";
  6203. }
  6204. $qrelJS .= " if (typeof sgqa !== 'undefined' && !LEMregexMatch('/ java' + sgqa + ' /', UsesVars)) {\n return;\n }\n";
  6205. $qrelJS .= implode("",$relParts);
  6206. $qrelJS .= "}\n";
  6207. $relEqns[] = $qrelJS;
  6208. $gseq_qidList[$arg['gseq']][$arg['qid']] = '1'; // means has an explicit LEMrel() function
  6209. }
  6210. }
  6211. foreach(array_keys($gseq_qidList) as $_gseq)
  6212. {
  6213. $relChangeVars[] = " relChangeG" . $_gseq . "=false;\n";
  6214. }
  6215. $jsParts[] = implode("",$relChangeVars);
  6216. // Process relevance for each group; and if group is relevant, process each contained question in order
  6217. foreach ($LEM->gRelInfo as $gr)
  6218. {
  6219. if (!array_key_exists($gr['gseq'],$gseqList)) {
  6220. continue;
  6221. }
  6222. if ($gr['relevancejs'] != '')
  6223. {
  6224. // $jsParts[] = "\n// Process Relevance for Group " . $gr['gid'];
  6225. // $jsParts[] = ": { " . $gr['eqn'] . " }";
  6226. $jsParts[] = "\nif (" . $gr['relevancejs'] . ") {\n";
  6227. $jsParts[] = " $('#group-" . $gr['gseq'] . "').show();\n";
  6228. $jsParts[] = " relChangeG" . $gr['gseq'] . "=true;\n";
  6229. $jsParts[] = " $('#relevanceG" . $gr['gseq'] . "').val(1);\n";
  6230. $qids = $gseq_qidList[$gr['gseq']];
  6231. foreach ($qids as $_qid=>$_val)
  6232. {
  6233. $qid2exclusiveAuto = (isset($LEM->qid2exclusiveAuto[$_qid]) ? $LEM->qid2exclusiveAuto[$_qid] : array());
  6234. if ($_val==1)
  6235. {
  6236. $jsParts[] = " LEMrel" . $_qid . "(sgqa);\n";
  6237. if (isset($LEM->qattr[$_qid]['exclude_all_others_auto']) && $LEM->qattr[$_qid]['exclude_all_others_auto'] == '1'
  6238. && isset($qid2exclusiveAuto['js']) && strlen($qid2exclusiveAuto['js']) > 0)
  6239. {
  6240. $jsParts[] = $qid2exclusiveAuto['js'];
  6241. $vars = explode('|',$qid2exclusiveAuto['relevanceVars']);
  6242. if (is_array($vars))
  6243. {
  6244. $allJsVarsUsed = array_merge($allJsVarsUsed,$vars);
  6245. }
  6246. if (!isset($rowdividList[$qid2exclusiveAuto['rowdivid']]))
  6247. {
  6248. $rowdividList[$qid2exclusiveAuto['rowdivid']] = true;
  6249. }
  6250. }
  6251. if (isset($LEM->qattr[$_qid]['exclude_all_others']))
  6252. {
  6253. foreach (explode(';',trim($LEM->qattr[$_qid]['exclude_all_others'])) as $eo)
  6254. {
  6255. // then need to call the function twice so that cascading of array filter onto an excluded option works
  6256. $jsParts[] = " LEMrel" . $_qid . "(sgqa);\n";
  6257. }
  6258. }
  6259. }
  6260. }
  6261. $jsParts[] = "}\nelse {\n";
  6262. $jsParts[] = " $('#group-" . $gr['gseq'] . "').hide();\n";
  6263. $jsParts[] = " if ($('#relevanceG" . $gr['gseq'] . "').val()=='1') { relChangeG" . $gr['gseq'] . "=true; }\n";
  6264. $jsParts[] = " $('#relevanceG" . $gr['gseq'] . "').val(0);\n";
  6265. $jsParts[] = "}\n";
  6266. }
  6267. else
  6268. {
  6269. $qids = $gseq_qidList[$gr['gseq']];
  6270. foreach ($qids as $_qid=>$_val)
  6271. {
  6272. $qid2exclusiveAuto = (isset($LEM->qid2exclusiveAuto[$_qid]) ? $LEM->qid2exclusiveAuto[$_qid] : array());
  6273. if ($_val == 1)
  6274. {
  6275. $jsParts[] = " LEMrel" . $_qid . "(sgqa);\n";
  6276. if (isset($LEM->qattr[$_qid]['exclude_all_others_auto']) && $LEM->qattr[$_qid]['exclude_all_others_auto'] == '1'
  6277. && isset($qid2exclusiveAuto['js']) && strlen($qid2exclusiveAuto['js']) > 0)
  6278. {
  6279. $jsParts[] = $qid2exclusiveAuto['js'];
  6280. $vars = explode('|',$qid2exclusiveAuto['relevanceVars']);
  6281. if (is_array($vars))
  6282. {
  6283. $allJsVarsUsed = array_merge($allJsVarsUsed,$vars);
  6284. }
  6285. if (!isset($rowdividList[$qid2exclusiveAuto['rowdivid']]))
  6286. {
  6287. $rowdividList[$qid2exclusiveAuto['rowdivid']] = true;
  6288. }
  6289. }
  6290. if (isset($LEM->qattr[$_qid]['exclude_all_others']))
  6291. {
  6292. foreach (explode(';',trim($LEM->qattr[$_qid]['exclude_all_others'])) as $eo)
  6293. {
  6294. // then need to call the function twice so that cascading of array filter onto an excluded option works
  6295. $jsParts[] = " LEMrel" . $_qid . "(sgqa);\n";
  6296. }
  6297. }
  6298. }
  6299. }
  6300. }
  6301. // Add logic for all-in-one mode to show/hide groups as long as at there is at least one relevant question within the group
  6302. // Only do this if there is no explicit group-level relevance equation, else may override group-level relevance
  6303. $dynamicQidsInG = (isset($dynamicQinG[$gr['gseq']]) ? $dynamicQinG[$gr['gseq']] : array());
  6304. $GalwaysVisible = (isset($GalwaysRelevant[$gr['gseq']]) ? $GalwaysRelevant[$gr['gseq']] : false);
  6305. if ($LEM->surveyMode == 'survey' && !$GalwaysVisible && count($dynamicQidsInG) > 0 && strlen(trim($gr['relevancejs']))== 0)
  6306. {
  6307. // check whether any dependent questions have changed
  6308. $relStatusTest = "($('#relevance" . implode("').val()=='1' || $('#relevance", array_keys($dynamicQidsInG)) . "').val()=='1')";
  6309. $jsParts[] = "\nif (" . $relStatusTest . ") {\n";
  6310. $jsParts[] = " $('#group-" . $gr['gseq'] . "').show();\n";
  6311. $jsParts[] = " if ($('#relevanceG" . $gr['gseq'] . "').val()=='0') { relChangeG" . $gr['gseq'] . "=true; }\n";
  6312. $jsParts[] = " $('#relevanceG" . $gr['gseq'] . "').val(1);\n";
  6313. $jsParts[] = "}\nelse {\n";
  6314. $jsParts[] = " $('#group-" . $gr['gseq'] . "').hide();\n";
  6315. $jsParts[] = " if ($('#relevanceG" . $gr['gseq'] . "').val()=='1') { relChangeG" . $gr['gseq'] . "=true; }\n";
  6316. $jsParts[] = " $('#relevanceG" . $gr['gseq'] . "').val(0);\n";
  6317. $jsParts[] = "}\n";
  6318. }
  6319. // now make sure any needed variables are accessible
  6320. $vars = explode('|',$gr['relevanceVars']);
  6321. if (is_array($vars))
  6322. {
  6323. $allJsVarsUsed = array_merge($allJsVarsUsed,$vars);
  6324. }
  6325. }
  6326. $jsParts[] = "\n}\n";
  6327. $jsParts[] = implode("\n",$relEqns);
  6328. $jsParts[] = implode("\n",$valEqns);
  6329. $allJsVarsUsed = array_unique($allJsVarsUsed);
  6330. // Add JavaScript Mapping Arrays
  6331. if (isset($LEM->alias2varName) && count($LEM->alias2varName) > 0)
  6332. {
  6333. $neededAliases=array();
  6334. $neededCanonical=array();
  6335. $neededCanonicalAttr=array();
  6336. foreach ($allJsVarsUsed as $jsVar)
  6337. {
  6338. if ($jsVar == '') {
  6339. continue;
  6340. }
  6341. if (preg_match("/^.*\.NAOK$/", $jsVar)) {
  6342. $jsVar = preg_replace("/\.NAOK$/","",$jsVar);
  6343. }
  6344. $neededCanonical[] = $jsVar;
  6345. foreach ($LEM->alias2varName as $key=>$value)
  6346. {
  6347. if ($jsVar == $value['jsName'])
  6348. {
  6349. $neededAliases[] = $value['jsPart'];
  6350. }
  6351. }
  6352. }
  6353. $neededCanonical = array_unique($neededCanonical);
  6354. foreach ($neededCanonical as $nc)
  6355. {
  6356. $neededCanonicalAttr[] = $LEM->varNameAttr[$nc];
  6357. }
  6358. $neededAliases = array_unique($neededAliases);
  6359. if (count($neededAliases) > 0)
  6360. {
  6361. $jsParts[] = "var LEMalias2varName = {\n";
  6362. $jsParts[] = implode(",\n",$neededAliases);
  6363. $jsParts[] = "};\n";
  6364. }
  6365. if (count($neededCanonicalAttr) > 0)
  6366. {
  6367. $jsParts[] = "var LEMvarNameAttr = {\n";
  6368. $jsParts[] = implode(",\n",$neededCanonicalAttr);
  6369. $jsParts[] = "};\n";
  6370. }
  6371. }
  6372. $jsParts[] = "//-->\n</script>\n";
  6373. // Now figure out which variables have not been declared (those not on the current page)
  6374. $undeclaredJsVars = array();
  6375. $undeclaredVal = array();
  6376. if (!$LEM->allOnOnePage)
  6377. {
  6378. foreach ($LEM->knownVars as $key=>$knownVar)
  6379. {
  6380. if (!is_numeric($key[0])) {
  6381. continue;
  6382. }
  6383. if ($knownVar['jsName'] == '') {
  6384. continue;
  6385. }
  6386. foreach ($allJsVarsUsed as $jsVar)
  6387. {
  6388. if ($jsVar == $knownVar['jsName'])
  6389. {
  6390. if ($LEM->surveyMode=='group' && $knownVar['gseq'] == $LEM->currentGroupSeq) {
  6391. if ($knownVar['hidden'] && $knownVar['type'] != '*') {
  6392. ; // need to declare a hidden variable for non-equation hidden variables so can do dynamic lookup.
  6393. }
  6394. else {
  6395. continue;
  6396. }
  6397. }
  6398. if ($LEM->surveyMode=='question' && $knownVar['qid'] == $LEM->currentQID) {
  6399. continue;
  6400. }
  6401. $undeclaredJsVars[] = $jsVar;
  6402. $sgqa = $knownVar['sgqa'];
  6403. $codeValue = (isset($_SESSION[$LEM->sessid][$sgqa])) ? $_SESSION[$LEM->sessid][$sgqa] : '';
  6404. $undeclaredVal[$jsVar] = $codeValue;
  6405. if (isset($LEM->jsVar2qid[$jsVar])) {
  6406. $qidList[$LEM->jsVar2qid[$jsVar]] = $LEM->jsVar2qid[$jsVar];
  6407. }
  6408. }
  6409. }
  6410. }
  6411. $undeclaredJsVars = array_unique($undeclaredJsVars);
  6412. foreach ($undeclaredJsVars as $jsVar)
  6413. {
  6414. // TODO - is different type needed for text? Or process value to striphtml?
  6415. if ($jsVar == '') continue;
  6416. $jsParts[] = "<input type='hidden' id='" . $jsVar . "' name='" . substr($jsVar,4) . "' value='" . htmlspecialchars($undeclaredVal[$jsVar],ENT_QUOTES) . "'/>\n";
  6417. }
  6418. }
  6419. else
  6420. {
  6421. // For all-in-one mode, declare the always-hidden variables, since qanda will not be called for them.
  6422. foreach ($LEM->knownVars as $key=>$knownVar)
  6423. {
  6424. if (!is_numeric($key[0])) {
  6425. continue;
  6426. }
  6427. if ($knownVar['jsName'] == '') {
  6428. continue;
  6429. }
  6430. if ($knownVar['hidden'])
  6431. {
  6432. $jsVar = $knownVar['jsName'];
  6433. $undeclaredJsVars[] = $jsVar;
  6434. $sgqa = $knownVar['sgqa'];
  6435. $codeValue = (isset($_SESSION[$LEM->sessid][$sgqa])) ? $_SESSION[$LEM->sessid][$sgqa] : '';
  6436. $undeclaredVal[$jsVar] = $codeValue;
  6437. }
  6438. }
  6439. $undeclaredJsVars = array_unique($undeclaredJsVars);
  6440. foreach ($undeclaredJsVars as $jsVar)
  6441. {
  6442. if ($jsVar == '') continue;
  6443. $jsParts[] = "<input type='hidden' id='" . $jsVar . "' name='" . $jsVar . "' value='" . htmlspecialchars($undeclaredVal[$jsVar],ENT_QUOTES) . "'/>\n";
  6444. }
  6445. }
  6446. foreach ($qidList as $qid)
  6447. {
  6448. if (isset($_SESSION[$LEM->sessid]['relevanceStatus'])) {
  6449. $relStatus = (isset($_SESSION[$LEM->sessid]['relevanceStatus'][$qid]) ? $_SESSION[$LEM->sessid]['relevanceStatus'][$qid] : 1);
  6450. }
  6451. else {
  6452. $relStatus = 1;
  6453. }
  6454. $jsParts[] = "<input type='hidden' id='relevance" . $qid . "' name='relevance" . $qid . "' value='" . $relStatus . "'/>\n";
  6455. }
  6456. foreach ($gseqList as $gseq)
  6457. {
  6458. if (isset($_SESSION['relevanceStatus'])) {
  6459. $relStatus = (isset($_SESSION['relevanceStatus']['G' . $gseq]) ? $_SESSION['relevanceStatus']['G' . $gseq] : 1);
  6460. }
  6461. else {
  6462. $relStatus = 1;
  6463. }
  6464. $jsParts[] = "<input type='hidden' id='relevanceG" . $gseq . "' name='relevanceG" . $gseq . "' value='" . $relStatus . "'/>\n";
  6465. }
  6466. foreach ($rowdividList as $key=>$val)
  6467. {
  6468. $jsParts[] = "<input type='hidden' id='relevance" . $key . "' name='relevance" . $key . "' value='" . $val . "'/>\n";
  6469. }
  6470. $LEM->runtimeTimings[] = array(__METHOD__,(microtime(true) - $now));
  6471. return implode('',$jsParts);
  6472. }
  6473. static function setTempVars($vars)
  6474. {
  6475. $LEM =& LimeExpressionManager::singleton();
  6476. $LEM->tempVars = $vars;
  6477. }
  6478. /**
  6479. * Unit test strings containing expressions
  6480. */
  6481. static function UnitTestProcessStringContainingExpressions()
  6482. {
  6483. $vars = array(
  6484. 'name' => array('sgqa'=>'name', 'code'=>'Peter', 'jsName'=>'java61764X1X1', 'readWrite'=>'N', 'type'=>'X', 'question'=>'What is your first/given name?', 'qseq'=>10, 'gseq'=>1),
  6485. 'surname' => array('sgqa'=>'surname', 'code'=>'Smith', 'jsName'=>'java61764X1X1', 'readWrite'=>'Y', 'type'=>'X', 'question'=>'What is your last/surname?', 'qseq'=>20, 'gseq'=>1),
  6486. 'age' => array('sgqa'=>'age', 'code'=>45, 'jsName'=>'java61764X1X2', 'readWrite'=>'Y', 'type'=>'X', 'question'=>'How old are you?', 'qseq'=>30, 'gseq'=>2),
  6487. 'numKids' => array('sgqa'=>'numKids', 'code'=>2, 'jsName'=>'java61764X1X3', 'readWrite'=>'Y', 'type'=>'X', 'question'=>'How many kids do you have?', 'relevance'=>'1', 'qid'=>'40','qseq'=>40, 'gseq'=>2),
  6488. 'numPets' => array('sgqa'=>'numPets', 'code'=>1, 'jsName'=>'java61764X1X4', 'readWrite'=>'Y', 'type'=>'X','question'=>'How many pets do you have?', 'qseq'=>50, 'gseq'=>2),
  6489. 'gender' => array('sgqa'=>'gender', 'code'=>'M', 'jsName'=>'java61764X1X5', 'readWrite'=>'Y', 'type'=>'X', 'shown'=>'Male','question'=>'What is your gender (male/female)?', 'qseq'=>110, 'gseq'=>2),
  6490. 'notSetYet' => array('sgqa'=>'notSetYet', 'code'=>'?', 'jsName'=>'java61764X3X6', 'readWrite'=>'Y', 'type'=>'X', 'shown'=>'Unknown','question'=>'Who will win the next election?', 'qseq'=>200, 'gseq'=>3),
  6491. // Constants
  6492. '61764X1X1' => array('sgqa'=>'61764X1X1', 'code'=> '<Sergei>', 'jsName'=>'', 'readWrite'=>'N', 'type'=>'X', 'qseq'=>70, 'gseq'=>2),
  6493. '61764X1X2' => array('sgqa'=>'61764X1X2', 'code'=> 45, 'jsName'=>'', 'readWrite'=>'N', 'type'=>'X', 'qseq'=>80, 'gseq'=>2),
  6494. '61764X1X3' => array('sgqa'=>'61764X1X3', 'code'=> 2, 'jsName'=>'', 'readWrite'=>'N', 'type'=>'X', 'qseq'=>15, 'gseq'=>1),
  6495. '61764X1X4' => array('sgqa'=>'61764X1X4', 'code'=> 1, 'jsName'=>'', 'readWrite'=>'N', 'type'=>'X', 'qseq'=>100, 'gseq'=>2),
  6496. 'TOKEN:ATTRIBUTE_1' => array('code'=> 'worker', 'jsName'=>'', 'readWrite'=>'N', 'type'=>'X'),
  6497. );
  6498. $tests = <<<EOD
  6499. This example shows escaping of the curly braces: \{\{test\}\} {if(1==1,'{{test}}', '1 is not 1?')} should not throw any errors.
  6500. <b>Here is an example of OK syntax with tooltips</b><br />Hello {if(gender=='M','Mr.','Mrs.')} {surname}, it is now {date('g:i a',time())}. Do you know where your {sum(numPets,numKids)} chidren and pets are?
  6501. <b>Here are common errors so you can see the tooltips</b><br />Variables used before they are declared: {notSetYet}<br />Unknown Function: {iff(numPets>numKids,1,2)}<br />Unknown Variable: {sum(age,num_pets,numKids)}<br />Wrong # parameters: {sprintf()},{if(1,2)},{date()}<br />Assign read-only-vars:{TOKEN:ATTRIBUTE_1+=10},{name='Sally'}<br />Unbalanced parentheses: {pow(3,4},{(pow(3,4)},{pow(3,4))}
  6502. <b>Here is some of the unsupported syntax</b><br />No support for '++', '--', '%',';': {min(++age, --age,age % 2);}<br />Nor '|', '&', '^': {(sum(2 | 3,3 & 4,5 ^ 6)}}<br />Nor arrays: {name[2], name['mine']}
  6503. <b>Inline JavaScipt that forgot to add spaces after curly brace</b><br />[script type="text/javascript" language="Javascript"] var job='{TOKEN:ATTRIBUTE_1}'; if (job=='worker') {document.write('BOSSES');}[/script]
  6504. <b>Unknown/Misspelled Variables, Functions, and Operators</b><br />{if(sex=='M','Mr.','Mrs.')} {surname}, next year you will be {age++} years old.
  6505. <b>Warns if use = instead of == or perform value assignments</b><br>Hello, {if(gender='M','Mr.','Mrs.')} {surname}, next year you will be {age+=1} years old.
  6506. <b>Wrong number of arguments for functions:</b><br />{if(gender=='M','Mr.','Mrs.','Other')} {surname}, sum(age,numKids,numPets)={sum(age,numKids,numPets,)}
  6507. <b>Mismatched parentheses</b><br />pow(3,4)={pow(3,4)}<br />but these are wrong: {pow(3,4}, {(((pow(3,4)}, {pow(3,4))}
  6508. <b>Unsupported syntax</b><br />No support for '++', '--', '%',';': {min(++age, --age, age % 2);}<br />Nor '|', '&', '^': {(sum(2 | 3, 3 & 4, 5 ^ 6)}}<br />Nor arrays: {name[2], name['mine']}
  6509. <b>Invalid assignments</b><br />Assign values to equations or strings: {(3 + 4)=5}, {'hi'='there'}<br />Assign read-only vars: {TOKEN:ATTRIBUTE_1='boss'}, {name='Sally'}
  6510. <b>Values:</b><br />name={name}; surname={surname}<br />gender={gender}; age={age}; numPets={numPets}<br />numKids=INSERTANS:61764X1X3={numKids}={INSERTANS:61764X1X3}<br />TOKEN:ATTRIBUTE_1={TOKEN:ATTRIBUTE_1}
  6511. <b>Question attributes:</b><br />numKids.question={numKids.question}; Question#={numKids.qid}; .relevance={numKids.relevance}
  6512. <b>Math:</b><br/>5+7={5+7}; 2*pi={2*pi()}; sin(pi/2)={sin(pi()/2)}; max(age,numKids,numPets)={max(age,numKids,numPets)}
  6513. <b>Text Processing:</b><br />{str_replace('like','love','I like LimeSurvey')}<br />{ucwords('hi there')}, {name}<br />{implode('--',name,'this is','a convenient way','way to','concatenate strings')}
  6514. <b>Dates:</b><br />{name}, the current date/time is: {date('F j, Y, g:i a',time())}
  6515. <b>Conditional:</b><br />Hello, {if(gender=='M','Mr.','Mrs.')} {surname}, may I call you {name}?
  6516. <b>Tailored Paragraph:</b><br />{name}, you said that you are {age} years old, and that you have {numKids} {if((numKids==1),'child','children')} and {numPets} {if((numPets==1),'pet','pets')} running around the house. So, you have {numKids + numPets} wild {if((numKids + numPets ==1),'beast','beasts')} to chase around every day.<p>Since you have more {if((numKids > numPets),'children','pets')} than you do {if((numKids > numPets),'pets','children')}, do you feel that the {if((numKids > numPets),'pets','children')} are at a disadvantage?</p>
  6517. <b>EM processes within strings:</b><br />Here is your picture [img src='images/users_{name}_{surname}.jpg' alt='{if(gender=='M','Mr.','Mrs.')} {name} {surname}'/];
  6518. <b>EM doesn't process curly braces like these:</b><br />{name}, { this is not an expression}<br />{nor is this }, { nor this }<br />\{nor this\},{this\},\{or this }
  6519. {INSERTANS:61764X1X1}, you said that you are {INSERTANS:61764X1X2} years old, and that you have {INSERTANS:61764X1X3} {if((INSERTANS:61764X1X3==1),'child','children')} and {INSERTANS:61764X1X4} {if((INSERTANS:61764X1X4==1),'pet','pets')} running around the house. So, you have {INSERTANS:61764X1X3 + INSERTANS:61764X1X4} wild {if((INSERTANS:61764X1X3 + INSERTANS:61764X1X4 ==1),'beast','beasts')} to chase around every day.
  6520. Since you have more {if((INSERTANS:61764X1X3 > INSERTANS:61764X1X4),'children','pets')} than you do {if((INSERTANS:61764X1X3 > INSERTANS:61764X1X4),'pets','children')}, do you feel that the {if((INSERTANS:61764X1X3 > INSERTANS:61764X1X4),'pets','children')} are at a disadvantage?
  6521. {INSERTANS:61764X1X1}, you said that you are {INSERTANS:61764X1X2} years old, and that you have {INSERTANS:61764X1X3} {if((INSERTANS:61764X1X3==1),'child','children','kiddies')} and {INSERTANS:61764X1X4} {if((INSERTANS:61764X1X4==1),'pet','pets')} running around the house. So, you have {INSERTANS:61764X1X3 + INSERTANS:61764X1X4} wild {if((INSERTANS:61764X1X3 + INSERTANS:61764X1X4 ==1),'beast','beasts')} to chase around every day.
  6522. This line should throw errors since the curly-brace enclosed functions do not have linefeeds after them (and before the closing curly brace): var job='{TOKEN:ATTRIBUTE_1}'; if (job=='worker') { document.write('BOSSES') } else { document.write('WORKERS') }
  6523. This line has a script section, but if you look at the source, you will see that it has errors: <script type="text/javascript" language="Javascript">var job='{TOKEN:ATTRIBUTE_1}'; if (job=='worker') {document.write('BOSSES')} else {document.write('WORKERS')} </script>.
  6524. Substitions that begin or end with a space should be ignored: { name} {age }
  6525. EOD;
  6526. $alltests = explode("\n",$tests);
  6527. $javascript1 = <<<EOST
  6528. var job='{TOKEN:ATTRIBUTE_1}';
  6529. if (job=='worker') {
  6530. document.write('BOSSES')
  6531. } else {
  6532. document.write('WORKERS')
  6533. }
  6534. EOST;
  6535. $javascript2 = <<<EOST
  6536. var job='{TOKEN:ATTRIBUTE_1}';
  6537. if (job=='worker') {
  6538. document.write('BOSSES')
  6539. } else { document.write('WORKERS') }
  6540. EOST;
  6541. $alltests[] = 'This line should have no errors - the Javascript has curly braces followed by line feeds:' . $javascript1;
  6542. $alltests[] = 'This line should also be OK: ' . $javascript2;
  6543. $alltests[] = 'This line has a hidden script: <script type="text/javascript" language="Javascript">' . $javascript1 . '</script>';
  6544. $alltests[] = 'This line has a hidden script: <script type="text/javascript" language="Javascript">' . $javascript2 . '</script>';
  6545. LimeExpressionManager::StartProcessingPage();
  6546. LimeExpressionManager::StartProcessingGroup(1);
  6547. $LEM =& LimeExpressionManager::singleton();
  6548. $LEM->tempVars = $vars;
  6549. $LEM->questionId2questionSeq = array();
  6550. $LEM->questionId2groupSeq = array();
  6551. $_SESSION[$LEM->sessid]['relevanceStatus'] = array();
  6552. foreach ($vars as $var) {
  6553. if (isset($var['qseq'])) {
  6554. $LEM->questionId2questionSeq[$var['qseq']] = $var['qseq'];
  6555. $LEM->questionId2groupSeq[$var['qseq']] = $var['gseq'];
  6556. $_SESSION[$LEM->sessid]['relevanceStatus'][$var['qseq']] = 1;
  6557. }
  6558. }
  6559. print "<h3>Note, if the <i>Vars Used</i> column is red, then at least one error was found in the <b>Source</b>. In such cases, the <i>Vars Used</i> list may be missing names of variables from sub-expressions containing errors</h3>";
  6560. print '<table border="1"><tr><th>Source</th><th>Pretty Print</th><th>Result</th><th>Vars Used</th></tr>';
  6561. for ($i=0;$i<count($alltests);++$i)
  6562. {
  6563. $test = $alltests[$i];
  6564. $result = LimeExpressionManager::ProcessString($test, 40, NULL, false, 1, 1);
  6565. $prettyPrint = LimeExpressionManager::GetLastPrettyPrintExpression();
  6566. $varsUsed = $LEM->em->GetAllVarsUsed();
  6567. if (count($varsUsed) > 0) {
  6568. sort($varsUsed);
  6569. $varList = implode(',<br />', $varsUsed);
  6570. }
  6571. else {
  6572. $varList = '&nbsp;';
  6573. }
  6574. print "<tr><td>" . htmlspecialchars($test,ENT_QUOTES) . "</td>\n";
  6575. print "<td>" . $prettyPrint . "</td>\n";
  6576. print "<td>" . $result . "</td>\n";
  6577. if ($LEM->em->HasErrors()) {
  6578. print "<td style='background-color: red'>";
  6579. }
  6580. else {
  6581. print "<td>";
  6582. }
  6583. print $varList . "</td>\n";
  6584. print "</tr>\n";
  6585. }
  6586. print '</table>';
  6587. LimeExpressionManager::FinishProcessingGroup();
  6588. LimeExpressionManager::FinishProcessingPage();
  6589. }
  6590. /**
  6591. * Unit test Relevance using a simplified syntax to represent questions.
  6592. */
  6593. static function UnitTestRelevance()
  6594. {
  6595. // Tests: varName~relevance~inputType~message
  6596. $tests = <<<EOT
  6597. name~1~text~What is your name?
  6598. age~1~text~How old are you (must be 16-80)?
  6599. badage~1~expr~{badage=((age<16) || (age>80))}
  6600. agestop~!is_empty(age) && ((age<16) || (age>80))~message~Sorry, {name}, you are too {if((age<16),'young',if((age>80),'old','middle-aged'))} for this test.
  6601. kids~!((age<16) || (age>80))~yesno~Do you have children (Y/N)?
  6602. kidsO~!is_empty(kids) && !(kids=='Y' or kids=='N')~message~Please answer the question about whether you have children with 'Y' or 'N'.
  6603. wantsKids~kids=='N'~yesno~Do you hope to have kids some day (Y/N)?
  6604. wantsKidsY~wantsKids=='Y'~message~{name}, I hope you are able to have children some day!
  6605. wantsKidsN~wantsKids=='N'~message~{name}, I hope you have a wonderfully fulfilling life!
  6606. wantsKidsO~!is_empty(wantsKids) && !(wantsKids=='Y' or wantsKids=='N')~message~Please answer the question about whether you want children with 'Y' or 'N'.
  6607. parents~1~expr~{parents = (!badage && kids=='Y')}
  6608. numKids~kids=='Y'~text~How many children do you have?
  6609. numKidsValidation~parents and strlen(numKids) > 0 and numKids <= 0~message~{name}, please check your entries. You said you do have children, {numKids} of them, which makes no sense.
  6610. kid1~numKids >= 1~text~How old is your first child?
  6611. kid2~numKids >= 2~text~How old is your second child?
  6612. kid3~numKids >= 3~text~How old is your third child?
  6613. kid4~numKids >= 4~text~How old is your fourth child?
  6614. kid5~numKids >= 5~text~How old is your fifth child?
  6615. sumage~1~expr~{sumage=sum(kid1.NAOK,kid2.NAOK,kid3.NAOK,kid4.NAOK,kid5.NAOK)}
  6616. report~numKids > 0~message~{name}, you said you are {age} and that you have {numKids} kids. The sum of ages of your first {min(numKids,5)} kids is {sumage}.
  6617. EOT;
  6618. $vars = array();
  6619. $varsNAOK = array();
  6620. $varSeq = array();
  6621. $testArgs = array();
  6622. $argInfo = array();
  6623. LimeExpressionManager::SetDirtyFlag();
  6624. $LEM =& LimeExpressionManager::singleton();
  6625. LimeExpressionManager::StartProcessingPage(true);
  6626. LimeExpressionManager::StartProcessingGroup(1); // pretending this is group 1
  6627. // collect variables
  6628. $i=0;
  6629. foreach(explode("\n",$tests) as $test)
  6630. {
  6631. $args = explode("~",$test);
  6632. $type = (($args[1]=='expr') ? '*' : ($args[1]=='message') ? 'X' : 'S');
  6633. $vars[$args[0]] = array('sgqa'=>$args[0], 'code'=>'', 'jsName'=>'java' . $args[0], 'jsName_on'=>'java' . $args[0], 'readWrite'=>'Y', 'type'=>$type, 'relevanceStatus'=>'1', 'gid'=>1, 'gseq'=>1, 'qseq'=>$i, 'qid'=>$i);
  6634. $varSeq[] = $args[0];
  6635. $testArgs[] = $args;
  6636. $LEM->questionId2questionSeq[$i] = $i;
  6637. $LEM->questionId2groupSeq[$i] = 1;
  6638. $LEM->questionSeq2relevance[$i] = array(
  6639. 'relevance'=>htmlspecialchars(preg_replace('/[[:space:]]/',' ',$args[1]),ENT_QUOTES),
  6640. 'qid'=>$i,
  6641. 'qseq'=>$i,
  6642. 'gseq'=>1,
  6643. 'jsResultVar'=>'java' . $args[0],
  6644. 'type'=>$type,
  6645. 'hidden'=>false,
  6646. 'gid'=>1, // ($i % 3),
  6647. );
  6648. ++$i;
  6649. }
  6650. $LEM->knownVars = $vars;
  6651. $LEM->gRelInfo[1] = array(
  6652. 'gid' => 1,
  6653. 'gseq' => 1,
  6654. 'eqn' => '',
  6655. 'result' => 1,
  6656. 'numJsVars' => 0,
  6657. 'relevancejs' => '',
  6658. 'relevanceVars' => '',
  6659. 'prettyPrint'=> '',
  6660. );
  6661. $LEM->ProcessAllNeededRelevance();
  6662. // collect relevance
  6663. $alias2varName = array();
  6664. $varNameAttr = array();
  6665. for ($i=0;$i<count($testArgs);++$i)
  6666. {
  6667. $testArg = $testArgs[$i];
  6668. $var = $testArg[0];
  6669. $rel = LimeExpressionManager::QuestionIsRelevant($i);
  6670. $question = LimeExpressionManager::ProcessString($testArg[3], $i, NULL, true, 1, 1);
  6671. $jsVarName='java' . $testArg[0];
  6672. $argInfo[] = array(
  6673. 'num' => $i,
  6674. 'name' => $jsVarName,
  6675. 'sgqa' => $testArg[0],
  6676. 'type' => $testArg[2],
  6677. 'question' => $question,
  6678. 'relevance' => $testArg[1],
  6679. 'relevanceStatus' => $rel
  6680. );
  6681. $alias2varName[$var] = array('jsName'=>$jsVarName, 'jsPart' => "'" . $var . "':'" . $jsVarName . "'");
  6682. $alias2varName[$jsVarName] = array('jsName'=>$jsVarName, 'jsPart' => "'" . $jsVarName . "':'" . $jsVarName . "'");
  6683. $varNameAttr[$jsVarName] = "'" . $jsVarName . "':{"
  6684. . "'jsName':'" . $jsVarName
  6685. . "','jsName_on':'" . $jsVarName
  6686. . "','sgqa':'" . substr($jsVarName,4)
  6687. . "','qid':" . $i
  6688. . ",'gid':". 1 // ($i % 3) // so have 3 possible group numbers
  6689. . "}";
  6690. }
  6691. $LEM->alias2varName = $alias2varName;
  6692. $LEM->varNameAttr = $varNameAttr;
  6693. LimeExpressionManager::FinishProcessingGroup();
  6694. LimeExpressionManager::FinishProcessingPage();
  6695. print <<< EOD
  6696. <script type='text/javascript'>
  6697. <!--
  6698. var LEMradix='.';
  6699. function checkconditions(value, name, type, evt_type)
  6700. {
  6701. if (typeof evt_type === 'undefined')
  6702. {
  6703. evt_type = 'onchange';
  6704. }
  6705. ExprMgr_process_relevance_and_tailoring(evt_type,name,type);
  6706. }
  6707. // -->
  6708. </script>
  6709. EOD;
  6710. print LimeExpressionManager::GetRelevanceAndTailoringJavaScript();
  6711. // Print Table of questions
  6712. print "<h3>This is a test of dynamic relevance.</h3>";
  6713. print "Enter your name and age, and try all the permutations of answers to whether you have or want children.<br />\n";
  6714. print "Note how the text and sum of ages changes dynamically; that prior answers are remembered; and that irrelevant values are not included in the sum of ages.<br />";
  6715. print "<table border='1'><tr><td>";
  6716. foreach ($argInfo as $arg)
  6717. {
  6718. $rel = LimeExpressionManager::QuestionIsRelevant($arg['num']);
  6719. print "<div id='question" . $arg['num'] . (($rel) ? "'" : "' style='display: none'") . ">\n";
  6720. print "<input type='hidden' id='display" . $arg['num'] . "' name='" . $arg['num'] . "' value='" . (($rel) ? 'on' : '') . "'/>\n";
  6721. if ($arg['type'] == 'expr')
  6722. {
  6723. // Hack for testing purposes - rather than using LimeSurvey internals to store the results of equations, process them via a hidden <div>
  6724. print "<div style='display: none' id='hack_" . $arg['name'] . "'>" . $arg['question'];
  6725. print "<input type='hidden' id='" . $arg['name'] . "' name='" . $arg['name'] . "' value=''/></div>\n";
  6726. }
  6727. else {
  6728. print "<table border='1' width='100%'>\n<tr>\n<td>[Q" . $arg['num'] . "] " . $arg['question'] . "</td>\n";
  6729. switch($arg['type'])
  6730. {
  6731. case 'yesno':
  6732. case 'text':
  6733. print "<td><input type='text' id='" . $arg['name'] . "' name='" . $arg['sgqa'] . "' value='' onchange='checkconditions(this.value, this.name, this.type)'/></td>\n";
  6734. break;
  6735. case 'message':
  6736. print "<input type='hidden' id='" . $arg['name'] . "' name='" . $arg['sgqa'] . "' value=''/>\n";
  6737. break;
  6738. }
  6739. print "</tr>\n</table>\n";
  6740. }
  6741. print "</div>\n";
  6742. }
  6743. print "</table>";
  6744. LimeExpressionManager::SetDirtyFlag(); // so subsequent tests don't try to access these variables
  6745. }
  6746. /**
  6747. * Set the 'this' variable as an alias for SGQA within the code.
  6748. * @param <type> $sgqa
  6749. */
  6750. public static function SetThisAsAliasForSGQA($sgqa)
  6751. {
  6752. $LEM =& LimeExpressionManager::singleton();
  6753. if (isset($LEM->knownVars[$sgqa]))
  6754. {
  6755. $LEM->qcode2sgqa['this']=$sgqa;
  6756. }
  6757. }
  6758. public static function ShowStackTrace($msg=NULL,&$args=NULL)
  6759. {
  6760. $LEM =& LimeExpressionManager::singleton();
  6761. $msg = array("**Stack Trace**" . (is_null($msg) ? '' : ' - ' . $msg));
  6762. $count = 0;
  6763. foreach (debug_backtrace(false) as $log)
  6764. {
  6765. if ($count++ == 0){
  6766. continue; // skip this call
  6767. }
  6768. $LEM->debugStack = array();
  6769. $subargs=array();
  6770. if (!is_null($args) && $log['function'] == 'templatereplace') {
  6771. foreach ($args as $arg)
  6772. {
  6773. if (isset($log['args'][2][$arg])) {
  6774. $subargs[$arg] = $log['args'][2][$arg];
  6775. }
  6776. }
  6777. if (count($subargs) > 0) {
  6778. $arglist = print_r($subargs,true);
  6779. }
  6780. else {
  6781. $arglist = '';
  6782. }
  6783. }
  6784. else {
  6785. $arglist = '';
  6786. }
  6787. $msg[] = ' '
  6788. . (isset($log['file']) ? '[' . basename($log['file']) . ']': '')
  6789. . (isset($log['class']) ? $log['class'] : '')
  6790. . (isset($log['type']) ? $log['type'] : '')
  6791. . (isset($log['function']) ? $log['function'] : '')
  6792. . (isset($log['line']) ? '[' . $log['line'] . ']' : '')
  6793. . $arglist;
  6794. }
  6795. }
  6796. private function gT($string, $escapemode = 'html')
  6797. {
  6798. // eventually replace this with i8n
  6799. if (isset(Yii::app()->lang))
  6800. {
  6801. return Yii::app()->lang->gT($string, $escapemode);
  6802. }
  6803. else
  6804. {
  6805. return $string;
  6806. }
  6807. }
  6808. private function ngT($single, $plural, $number, $escapemode = 'html')
  6809. {
  6810. // eventually replace this with i8n
  6811. if (isset(Yii::app()->lang))
  6812. {
  6813. return Yii::app()->lang->ngT($single, $plural, $number, $escapemode);
  6814. }
  6815. else
  6816. {
  6817. return $string;
  6818. }
  6819. }
  6820. /**
  6821. * Returns true if the survey is using comma as the radix
  6822. * @return type
  6823. */
  6824. public static function usingCommaAsRadix()
  6825. {
  6826. $LEM =& LimeExpressionManager::singleton();
  6827. $usingCommaAsRadix = (($LEM->surveyOptions['radix']==',') ? true : false);
  6828. return $usingCommaAsRadix;
  6829. }
  6830. private static function getConditionsForEM($surveyid=NULL, $qid=NULL)
  6831. {
  6832. if (!is_null($qid)) {
  6833. $where = " c.qid = ".$qid." and ";
  6834. }
  6835. else if (!is_null($surveyid)) {
  6836. $where = " c.qid in (select qid from {{questions}} where sid = ".$surveyid.") and ";
  6837. }
  6838. else {
  6839. $where = "";
  6840. }
  6841. $query = "select distinct c.*"
  6842. .", q.sid, q.type"
  6843. ." from {{conditions}} as c"
  6844. .", {{questions}} as q"
  6845. ." where " . $where
  6846. ." c.cqid=q.qid"
  6847. ." union "
  6848. ." select c.*, q.sid, '' as type"
  6849. ." from {{conditions}} as c"
  6850. .", {{questions}} as q"
  6851. ." where ". $where
  6852. ." c.cqid = 0 and c.qid = q.qid";
  6853. $databasetype = Yii::app()->db->getDriverName();
  6854. if ($databasetype=='mssql')
  6855. {
  6856. $query .= " order by sid, c.qid, scenario, cqid, cfieldname, value";
  6857. }
  6858. else
  6859. {
  6860. $query .= " order by sid, qid, scenario, cqid, cfieldname, value";
  6861. }
  6862. $data = dbExecuteAssoc($query);
  6863. return $data;
  6864. }
  6865. /**
  6866. * Deprecate obsolete question attributes.
  6867. * @param boolean $changedb - if true, updates parameters and deletes old ones
  6868. * @param type $surveyid - if set, then only for that survey
  6869. * @param type $onlythisqid - if set, then only for this question ID
  6870. */
  6871. public static function UpgradeQuestionAttributes($changeDB=false,$surveyid=NULL,$onlythisqid=NULL)
  6872. {
  6873. $LEM =& LimeExpressionManager::singleton();
  6874. $qattrs = $LEM->getQuestionAttributesForEM($surveyid,$onlythisqid);
  6875. $qupdates = array();
  6876. $attibutemap = array(
  6877. 'max_num_value_sgqa' => 'max_num_value',
  6878. 'min_num_value_sgqa' => 'min_num_value',
  6879. 'num_value_equals_sgqa' => 'equals_num_value',
  6880. );
  6881. $reverseAttributeMap = array_flip($attibutemap);
  6882. foreach ($qattrs as $qid => $qattr)
  6883. {
  6884. $updates = array();
  6885. foreach ($attibutemap as $src=>$target)
  6886. {
  6887. if (isset($qattr[$src]) && trim($qattr[$src]) != '')
  6888. {
  6889. $updates[$target] = $qattr[$src];
  6890. }
  6891. }
  6892. if (count($updates) > 0)
  6893. {
  6894. $qupdates[$qid] = $updates;
  6895. }
  6896. }
  6897. if ($changeDB)
  6898. {
  6899. $queries = array();
  6900. foreach ($qupdates as $qid=>$updates)
  6901. {
  6902. foreach ($updates as $key=>$value)
  6903. {
  6904. $query = "UPDATE {{question_attributes}} SET value=".Yii::app()->db->quoteValue($value)." WHERE qid=".$qid." and attribute=".Yii::app()->db->quoteValue($key);
  6905. $queries[] = $query;
  6906. $query = "DELETE FROM {{question_attributes}} WHERE qid=".$qid." and attribute=".Yii::app()->db->quoteValue($reverseAttributeMap[$key]);
  6907. $queries[] = $query;
  6908. }
  6909. }
  6910. // now update the datbase
  6911. foreach ($queries as $query)
  6912. {
  6913. dbExecuteAssoc($query);
  6914. }
  6915. return $queries;
  6916. }
  6917. else
  6918. {
  6919. return $qupdates;
  6920. }
  6921. }
  6922. private function getQuestionAttributesForEM($surveyid=NULL,$qid=NULL, $lang=NULL)
  6923. {
  6924. if (!is_null($qid)) {
  6925. $where = " a.qid = ".$qid." and a.qid=b.qid";
  6926. }
  6927. else if (!is_null($surveyid)) {
  6928. $where = " a.qid=b.qid and b.sid=".$surveyid;
  6929. }
  6930. else {
  6931. $where = " a.qid=b.qid";
  6932. }
  6933. if (!is_null($lang)) {
  6934. $lang = " and a.language='".$lang."' and b.language='".$lang."'";
  6935. }
  6936. $databasetype = Yii::app()->db->getDriverName();
  6937. if ($databasetype=='mssql' || $databasetype=="sqlsrv")
  6938. {
  6939. $query = "select distinct a.qid, a.attribute, CAST(a.value as varchar(max)) as value";
  6940. }
  6941. else
  6942. {
  6943. $query = "select distinct a.qid, a.attribute, a.value";
  6944. }
  6945. $query .= " from {{question_attributes}} as a, {{questions}} as b"
  6946. ." where " . $where
  6947. .$lang
  6948. ." order by a.qid, a.attribute";
  6949. $data = dbExecuteAssoc($query);
  6950. $qattr = array();
  6951. foreach($data->readAll() as $row) {
  6952. $qattr[$row['qid']][$row['attribute']] = $row['value'];
  6953. }
  6954. if (!is_null($lang))
  6955. {
  6956. // Then get non-language specific first, and overwrite with language-specific
  6957. $qattr2 = $qattr;
  6958. $qattr = $this->getQuestionAttributesForEM($surveyid,$qid);
  6959. foreach ($qattr2 as $q => $qattrs) {
  6960. if (isset($qattrs) && is_array($qattrs)) {
  6961. foreach ($qattrs as $attr=>$value) {
  6962. $qattr[$q][$attr] = $value;
  6963. }
  6964. }
  6965. }
  6966. }
  6967. return $qattr;
  6968. }
  6969. /**
  6970. * Return array of language-specific answer codes
  6971. * @param <type> $surveyid
  6972. * @param <type> $qid
  6973. * @return <type>
  6974. */
  6975. function getAnswerSetsForEM($surveyid=NULL,$qid=NULL,$lang=NULL)
  6976. {
  6977. if (!is_null($qid)) {
  6978. $where = "a.qid = ".$qid;
  6979. }
  6980. else if (!is_null($surveyid)) {
  6981. $where = "a.qid = q.qid and q.sid = ".$surveyid;
  6982. }
  6983. else {
  6984. $where = "1";
  6985. }
  6986. if (!is_null($lang)) {
  6987. $lang = " and a.language='".$lang."' and q.language='".$lang."'";
  6988. }
  6989. $query = "SELECT a.qid, a.code, a.answer, a.scale_id, a.assessment_value"
  6990. ." FROM {{answers}} AS a, {{questions}} as q"
  6991. ." WHERE ".$where
  6992. .$lang
  6993. ." ORDER BY a.qid, a.scale_id, a.sortorder";
  6994. $data = dbExecuteAssoc($query);
  6995. $qans = array();
  6996. $useAssessments = ((isset($this->surveyOptions['assessments'])) ? $this->surveyOptions['assessments'] : false);
  6997. foreach($data->readAll() as $row) {
  6998. if (!isset($qans[$row['qid']])) {
  6999. $qans[$row['qid']] = array();
  7000. }
  7001. $qans[$row['qid']][$row['scale_id'].'~'.$row['code']] = ($useAssessments ? $row['assessment_value'] : $row['code']) . '|' . $row['answer'];
  7002. }
  7003. return $qans;
  7004. }
  7005. /**
  7006. * Returns group info needed for indexes
  7007. * @param <type> $surveyid
  7008. * @param string $lang
  7009. * @return <type>
  7010. */
  7011. function getGroupInfoForEM($surveyid,$lang=NULL)
  7012. {
  7013. if (!is_null($lang)) {
  7014. $lang = " and a.language='".$lang."'";
  7015. }
  7016. $query = "SELECT a.group_name, a.description, a.gid, a.group_order, a.grelevance"
  7017. ." FROM {{groups}} AS a"
  7018. ." WHERE a.sid=".$surveyid
  7019. .$lang
  7020. ." ORDER BY group_order";
  7021. $data = dbExecuteAssoc($query);
  7022. $qinfo = array();
  7023. $_order=0;
  7024. foreach ($data as $d)
  7025. {
  7026. $gid[$d['gid']] = array(
  7027. 'group_order' => $_order,
  7028. 'gid' => $d['gid'],
  7029. 'group_name' => $d['group_name'],
  7030. 'description' => $d['description'],
  7031. 'grelevance' => $d['grelevance'],
  7032. );
  7033. $qinfo[$_order] = $gid[$d['gid']];
  7034. ++$_order;
  7035. }
  7036. if (isset($_SESSION['survey_'.$surveyid]) && isset($_SESSION['survey_'.$surveyid]['grouplist'])) {
  7037. $_order=0;
  7038. $qinfo = array();
  7039. foreach ($_SESSION['survey_'.$surveyid]['grouplist'] as $info)
  7040. {
  7041. $gid[$info['gid']]['group_order'] = $_order;
  7042. $qinfo[$_order] = $gid[$info['gid']];
  7043. ++$_order;
  7044. }
  7045. }
  7046. return $qinfo;
  7047. }
  7048. /**
  7049. * Cleanse the $_POSTed data and update $_SESSION variables accordingly
  7050. */
  7051. static function ProcessCurrentResponses()
  7052. {
  7053. $LEM =& LimeExpressionManager::singleton();
  7054. if (!isset($LEM->currentQset)) {
  7055. return array();
  7056. }
  7057. $updatedValues=array();
  7058. $radixchange = (($LEM->surveyOptions['radix']==',') ? true : false);
  7059. foreach ($LEM->currentQset as $qinfo)
  7060. {
  7061. $relevant=false;
  7062. $qid = $qinfo['info']['qid'];
  7063. $gseq = $qinfo['info']['gseq'];
  7064. $relevant = (isset($_POST['relevance' . $qid]) ? ($_POST['relevance' . $qid] == 1) : false);
  7065. $grelevant = (isset($_POST['relevanceG' . $gseq]) ? ($_POST['relevanceG' . $gseq] == 1) : false);
  7066. $_SESSION[$LEM->sessid]['relevanceStatus'][$qid] = $relevant;
  7067. $_SESSION[$LEM->sessid]['relevanceStatus']['G' . $gseq] = $grelevant;
  7068. foreach (explode('|',$qinfo['sgqa']) as $sq)
  7069. {
  7070. $sqrelevant=true;
  7071. if (isset($LEM->subQrelInfo[$qid][$sq]['rowdivid']))
  7072. {
  7073. $rowdivid = $LEM->subQrelInfo[$qid][$sq]['rowdivid'];
  7074. if ($rowdivid!='' && isset($_POST['relevance' . $rowdivid]))
  7075. {
  7076. $sqrelevant = ($_POST['relevance' . $rowdivid] == 1);
  7077. $_SESSION[$LEM->sessid]['relevanceStatus'][$rowdivid] = $sqrelevant;
  7078. }
  7079. }
  7080. $type = $qinfo['info']['type'];
  7081. if ($relevant && $grelevant && $sqrelevant)
  7082. {
  7083. if ($qinfo['info']['hidden'] && !isset($_POST[$sq]))
  7084. {
  7085. $value = (isset($_SESSION[$LEM->sessid][$sq]) ? $_SESSION[$LEM->sessid][$sq] : ''); // if always hidden, use the default value, if any
  7086. }
  7087. else
  7088. {
  7089. $value = (isset($_POST[$sq]) ? $_POST[$sq] : '');
  7090. }
  7091. if ($radixchange && isset($LEM->knownVars[$sq]['onlynum']) && $LEM->knownVars[$sq]['onlynum']=='1')
  7092. {
  7093. // convert from comma back to decimal
  7094. $value = implode('.',explode(',',$value));
  7095. }
  7096. switch($type)
  7097. {
  7098. case 'D': //DATE
  7099. if (trim($value)=="")
  7100. {
  7101. $value = "";
  7102. }
  7103. else
  7104. {
  7105. $aAttributes=$LEM->getQuestionAttributesForEM($LEM->sid, $qid,$_SESSION['LEMlang']);
  7106. if (!isset($aAttributes[$qid])) {
  7107. $aAttributes[$qid]=array();
  7108. }
  7109. $aDateFormatData=getDateFormatDataForQID($aAttributes[$qid],$LEM->surveyOptions);
  7110. $oDateTimeConverter = new Date_Time_Converter($value, $aDateFormatData['phpdate']);
  7111. $value=$oDateTimeConverter->convert("Y-m-d H:i");
  7112. }
  7113. break;
  7114. case 'N': //NUMERICAL QUESTION TYPE
  7115. case 'K': //MULTIPLE NUMERICAL QUESTION
  7116. if (trim($value)=="") {
  7117. $value = "";
  7118. }
  7119. else {
  7120. $value = sanitize_float($value);
  7121. }
  7122. break;
  7123. case '|': //File Upload
  7124. if (!preg_match('/_filecount$/', $sq))
  7125. {
  7126. $json = $value;
  7127. $phparray = json_decode(stripslashes($json));
  7128. // if the files have not been saved already,
  7129. // move the files from tmp to the files folder
  7130. $tmp = $LEM->surveyOptions['tempdir'] . 'upload'. DIRECTORY_SEPARATOR;
  7131. if (!is_null($phparray) && count($phparray) > 0)
  7132. {
  7133. // Move the (unmoved, temp) files from temp to files directory.
  7134. // Check all possible file uploads
  7135. for ($i = 0; $i < count($phparray); $i++)
  7136. {
  7137. if (file_exists($tmp . $phparray[$i]->filename))
  7138. {
  7139. $sDestinationFileName = 'fu_' . randomChars(15);
  7140. if (!is_dir($LEM->surveyOptions['target']))
  7141. {
  7142. mkdir($LEM->surveyOptions['target'], 0777, true);
  7143. }
  7144. if (!rename($tmp . $phparray[$i]->filename, $LEM->surveyOptions['target'] . $sDestinationFileName))
  7145. {
  7146. echo "Error moving file to target destination";
  7147. }
  7148. $phparray[$i]->filename = $sDestinationFileName;
  7149. }
  7150. }
  7151. $value = ls_json_encode($phparray); // so that EM doesn't try to parse it.
  7152. }
  7153. }
  7154. break;
  7155. }
  7156. $_SESSION[$LEM->sessid][$sq] = $value;
  7157. $_update = array (
  7158. 'type'=>$type,
  7159. 'value'=>$value,
  7160. );
  7161. $updatedValues[$sq] = $_update;
  7162. $LEM->updatedValues[$sq] = $_update;
  7163. }
  7164. else { // irrelevant, so database will be NULLed separately
  7165. // Must unset the value, rather than setting to '', so that EM can re-use the default value as needed.
  7166. unset($_SESSION[$LEM->sessid][$sq]);
  7167. $_update = array (
  7168. 'type'=>$type,
  7169. 'value'=>NULL,
  7170. );
  7171. $updatedValues[$sq] = $_update;
  7172. $LEM->updatedValues[$sq] = $_update;
  7173. }
  7174. }
  7175. }
  7176. if (isset($_POST['timerquestion']))
  7177. {
  7178. $_SESSION[$LEM->sessid][$_POST['timerquestion']]=sanitize_float($_POST[$_POST['timerquestion']]);
  7179. }
  7180. return $updatedValues;
  7181. }
  7182. static public function isValidVariable($varName)
  7183. {
  7184. $LEM =& LimeExpressionManager::singleton();
  7185. if (isset($LEM->knownVars[$varName]))
  7186. {
  7187. return true;
  7188. }
  7189. else if (isset($LEM->qcode2sgqa[$varName]))
  7190. {
  7191. return true;
  7192. }
  7193. else if (isset($LEM->tempVars[$varName]))
  7194. {
  7195. return true;
  7196. }
  7197. return false;
  7198. }
  7199. static public function GetVarAttribute($name,$attr,$default,$gseq,$qseq)
  7200. {
  7201. $LEM =& LimeExpressionManager::singleton();
  7202. return $LEM->_GetVarAttribute($name,$attr,$default,$gseq,$qseq);
  7203. }
  7204. private function _GetVarAttribute($name,$attr,$default,$gseq,$qseq)
  7205. {
  7206. $args = explode(".", $name);
  7207. $varName = $args[0];
  7208. $varName = preg_replace("/^(?:INSERTANS:)?(.*?)$/", "$1", $varName);
  7209. if (isset($this->knownVars[$varName]))
  7210. {
  7211. $var = $this->knownVars[$varName];
  7212. }
  7213. else if (isset($this->qcode2sgqa[$varName]))
  7214. {
  7215. $var = $this->knownVars[$this->qcode2sgqa[$varName]];
  7216. }
  7217. else if (isset($this->tempVars[$varName]))
  7218. {
  7219. $var = $this->tempVars[$varName];
  7220. }
  7221. else
  7222. {
  7223. return '{' . $name . '}';
  7224. }
  7225. $sgqa = isset($var['sgqa']) ? $var['sgqa'] : NULL;
  7226. if (is_null($attr))
  7227. {
  7228. // then use the requested attribute, if any
  7229. $_attr = 'code';
  7230. if (preg_match("/INSERTANS:/",$args[0]))
  7231. {
  7232. $_attr = 'shown';
  7233. }
  7234. $attr = (count($args)==2) ? $args[1] : $_attr;
  7235. }
  7236. // Like JavaScript, if an answer is irrelevant, always return ''
  7237. if (preg_match('/^code|NAOK|shown|valueNAOK|value$/',$attr) && isset($var['qid']) && $var['qid']!='')
  7238. {
  7239. if (!$this->_GetVarAttribute($varName,'relevanceStatus',false,$gseq,$qseq))
  7240. {
  7241. return '';
  7242. }
  7243. }
  7244. switch ($attr)
  7245. {
  7246. case 'varName':
  7247. return $name;
  7248. break;
  7249. case 'code':
  7250. case 'NAOK':
  7251. if (isset($var['code'])) {
  7252. return $var['code']; // for static values like TOKEN
  7253. }
  7254. else {
  7255. if (isset($_SESSION[$this->sessid][$sgqa])) {
  7256. $type = $var['type'];
  7257. switch($type)
  7258. {
  7259. case 'Q': //MULTIPLE SHORT TEXT
  7260. case ';': //ARRAY (Multi Flexi) Text
  7261. case 'S': //SHORT FREE TEXT
  7262. case 'T': //LONG FREE TEXT
  7263. case 'U': //HUGE FREE TEXT
  7264. return htmlspecialchars($_SESSION[$this->sessid][$sgqa],ENT_NOQUOTES);// Minimum sanitizing the string entered by user
  7265. case '!': //List - dropdown
  7266. case 'L': //LIST drop-down/radio-button list
  7267. case 'O': //LIST WITH COMMENT drop-down/radio-button list + textarea
  7268. case 'M': //Multiple choice checkbox
  7269. case 'P': //Multiple choice with comments checkbox + text
  7270. if (preg_match('/comment$/',$sgqa) || preg_match('/other$/',$sgqa) || preg_match('/_other$/',$name))
  7271. {
  7272. return htmlspecialchars($_SESSION[$this->sessid][$sgqa],ENT_NOQUOTES);// Minimum sanitizing the string entered by user
  7273. }
  7274. else
  7275. {
  7276. return $_SESSION[$this->sessid][$sgqa];
  7277. }
  7278. default:
  7279. return $_SESSION[$this->sessid][$sgqa];
  7280. }
  7281. }
  7282. elseif (isset($var['default']) && !is_null($var['default'])) {
  7283. return $var['default'];
  7284. }
  7285. return $default;
  7286. }
  7287. break;
  7288. case 'value':
  7289. case 'valueNAOK':
  7290. {
  7291. $type = $var['type'];
  7292. $code = $this->_GetVarAttribute($name,'code',$default,$gseq,$qseq);
  7293. switch($type)
  7294. {
  7295. case '!': //List - dropdown
  7296. case 'L': //LIST drop-down/radio-button list
  7297. case 'O': //LIST WITH COMMENT drop-down/radio-button list + textarea
  7298. case '1': //Array (Flexible Labels) dual scale // need scale
  7299. case 'H': //ARRAY (Flexible) - Column Format
  7300. case 'F': //ARRAY (Flexible) - Row Format
  7301. case 'R': //RANKING STYLE
  7302. if ($type == 'O' && preg_match('/comment\.value/',$name))
  7303. {
  7304. $value = $code;
  7305. }
  7306. else if (($type == 'L' || $type == '!') && preg_match('/_other\.value/',$name))
  7307. {
  7308. $value = $code;
  7309. }
  7310. else
  7311. {
  7312. $scale_id = $this->_GetVarAttribute($name,'scale_id','0',$gseq,$qseq);
  7313. $which_ans = $scale_id . '~' . $code;
  7314. $ansArray = $var['ansArray'];
  7315. if (is_null($ansArray))
  7316. {
  7317. $value = $default;
  7318. }
  7319. else
  7320. {
  7321. if (isset($ansArray[$which_ans])) {
  7322. $answerInfo = explode('|',$ansArray[$which_ans]);
  7323. $answer = $answerInfo[0];
  7324. }
  7325. else {
  7326. $answer = $default;
  7327. }
  7328. $value = $answer;
  7329. }
  7330. }
  7331. break;
  7332. default:
  7333. $value = $code;
  7334. break;
  7335. }
  7336. return $value;
  7337. }
  7338. break;
  7339. case 'jsName':
  7340. if ($this->surveyMode=='survey'
  7341. || ($this->surveyMode=='group' && $gseq != -1 && isset($var['gseq']) && $gseq == $var['gseq'])
  7342. || ($this->surveyMode=='question' && $qseq != -1 && isset($var['qseq']) && $qseq == $var['qseq']))
  7343. {
  7344. return (isset($var['jsName_on']) ? $var['jsName_on'] : (isset($var['jsName'])) ? $var['jsName'] : $default);
  7345. }
  7346. else {
  7347. return (isset($var['jsName']) ? $var['jsName'] : $default);
  7348. }
  7349. break;
  7350. case 'sgqa':
  7351. case 'mandatory':
  7352. case 'qid':
  7353. case 'gid':
  7354. case 'grelevance':
  7355. case 'question':
  7356. case 'readWrite':
  7357. case 'relevance':
  7358. case 'rowdivid':
  7359. case 'type':
  7360. case 'qcode':
  7361. case 'gseq':
  7362. case 'qseq':
  7363. case 'ansList':
  7364. case 'scale_id':
  7365. return (isset($var[$attr])) ? $var[$attr] : $default;
  7366. case 'shown':
  7367. if (isset($var['shown']))
  7368. {
  7369. return $var['shown']; // for static values like TOKEN
  7370. }
  7371. else
  7372. {
  7373. $type = $var['type'];
  7374. $code = $this->_GetVarAttribute($name,'code',$default,$gseq,$qseq);
  7375. switch($type)
  7376. {
  7377. case '!': //List - dropdown
  7378. case 'L': //LIST drop-down/radio-button list
  7379. case 'O': //LIST WITH COMMENT drop-down/radio-button list + textarea
  7380. case '1': //Array (Flexible Labels) dual scale // need scale
  7381. case 'H': //ARRAY (Flexible) - Column Format
  7382. case 'F': //ARRAY (Flexible) - Row Format
  7383. case 'R': //RANKING STYLE
  7384. if ($type == 'O' && preg_match('/comment$/',$name))
  7385. {
  7386. $shown = $code;
  7387. }
  7388. else if (($type == 'L' || $type == '!') && preg_match('/_other$/',$name))
  7389. {
  7390. $shown = $code;
  7391. }
  7392. else
  7393. {
  7394. $scale_id = $this->_GetVarAttribute($name,'scale_id','0',$gseq,$qseq);
  7395. $which_ans = $scale_id . '~' . $code;
  7396. $ansArray = $var['ansArray'];
  7397. if (is_null($ansArray))
  7398. {
  7399. $shown=$code;
  7400. }
  7401. else
  7402. {
  7403. if (isset($ansArray[$which_ans])) {
  7404. $answerInfo = explode('|',$ansArray[$which_ans]);
  7405. array_shift($answerInfo);
  7406. $answer = join('|',$answerInfo);
  7407. }
  7408. else {
  7409. $answer = $code;
  7410. }
  7411. $shown = $answer;
  7412. }
  7413. }
  7414. break;
  7415. case 'A': //ARRAY (5 POINT CHOICE) radio-buttons
  7416. case 'B': //ARRAY (10 POINT CHOICE) radio-buttons
  7417. case ':': //ARRAY (Multi Flexi) 1 to 10
  7418. case '5': //5 POINT CHOICE radio-buttons
  7419. $shown = $code;
  7420. break;
  7421. case 'N': //NUMERICAL QUESTION TYPE
  7422. case 'K': //MULTIPLE NUMERICAL QUESTION
  7423. case 'Q': //MULTIPLE SHORT TEXT
  7424. case ';': //ARRAY (Multi Flexi) Text
  7425. case 'S': //SHORT FREE TEXT
  7426. case 'T': //LONG FREE TEXT
  7427. case 'U': //HUGE FREE TEXT
  7428. case 'D': //DATE
  7429. case '*': //Equation
  7430. case 'I': //Language Question
  7431. case '|': //File Upload
  7432. case 'X': //BOILERPLATE QUESTION
  7433. $shown = $code;
  7434. break;
  7435. case 'M': //Multiple choice checkbox
  7436. case 'P': //Multiple choice with comments checkbox + text
  7437. if ($code == 'Y' && isset($var['question']) && !preg_match('/comment$/',$sgqa))
  7438. {
  7439. $shown = $var['question'];
  7440. }
  7441. elseif (preg_match('/comment$/',$sgqa)) {
  7442. $shown=$code; // This one return sgqa.code
  7443. }
  7444. else
  7445. {
  7446. $shown = $default;
  7447. }
  7448. break;
  7449. case 'G': //GENDER drop-down list
  7450. case 'Y': //YES/NO radio-buttons
  7451. case 'C': //ARRAY (YES/UNCERTAIN/NO) radio-buttons
  7452. case 'E': //ARRAY (Increase/Same/Decrease) radio-buttons
  7453. $ansArray = $var['ansArray'];
  7454. if (is_null($ansArray))
  7455. {
  7456. $shown=$default;
  7457. }
  7458. else
  7459. {
  7460. if (isset($ansArray[$code])) {
  7461. $answer = $ansArray[$code];
  7462. }
  7463. else {
  7464. $answer = $default;
  7465. }
  7466. $shown = $answer;
  7467. }
  7468. break;
  7469. }
  7470. return $shown;
  7471. }
  7472. case 'relevanceStatus':
  7473. $gseq = (isset($var['gseq'])) ? $var['gseq'] : -1;
  7474. $qid = (isset($var['qid'])) ? $var['qid'] : -1;
  7475. $rowdivid = (isset($var['rowdivid']) && $var['rowdivid']!='') ? $var['rowdivid'] : -1;
  7476. if ($qid == -1 || $gseq == -1) {
  7477. return 1;
  7478. }
  7479. if (isset($args[1]) && $args[1]=='NAOK') {
  7480. return 1;
  7481. }
  7482. $grel = (isset($_SESSION[$this->sessid]['relevanceStatus']['G'.$gseq]) ? $_SESSION[$this->sessid]['relevanceStatus']['G'.$gseq] : 1); // true by default
  7483. $qrel = (isset($_SESSION[$this->sessid]['relevanceStatus'][$qid]) ? $_SESSION[$this->sessid]['relevanceStatus'][$qid] : 0);
  7484. $sqrel = (isset($_SESSION[$this->sessid]['relevanceStatus'][$rowdivid]) ? $_SESSION[$this->sessid]['relevanceStatus'][$rowdivid] : 1); // true by default - only want false if a subquestion is irrelevant
  7485. return ($grel && $qrel && $sqrel);
  7486. default:
  7487. print 'UNDEFINED ATTRIBUTE: ' . $attr . "<br />\n";
  7488. return $default;
  7489. }
  7490. return $default; // and throw and error?
  7491. }
  7492. public static function SetVariableValue($op,$name,$value)
  7493. {
  7494. $LEM =& LimeExpressionManager::singleton();
  7495. if (isset($LEM->tempVars[$name]))
  7496. {
  7497. switch($op)
  7498. {
  7499. case '=':
  7500. $LEM->tempVars[$name]['code'] = $value;
  7501. break;
  7502. case '*=':
  7503. $LEM->tempVars[$name]['code'] *= $value;
  7504. break;
  7505. case '/=':
  7506. $LEM->tempVars[$name]['code'] /= $value;
  7507. break;
  7508. case '+=':
  7509. $LEM->tempVars[$name]['code'] += $value;
  7510. break;
  7511. case '-=':
  7512. $LEM->tempVars[$name]['code'] -= $value;
  7513. break;
  7514. }
  7515. $_result = $LEM->tempVars[$name]['code'];
  7516. $_SESSION[$LEM->sessid][$name] = $_result;
  7517. $LEM->updatedValues[$name] = array(
  7518. 'type'=>'*',
  7519. 'value'=>$_result,
  7520. );
  7521. return $_result;
  7522. }
  7523. else
  7524. {
  7525. if (!isset($LEM->knownVars[$name]))
  7526. {
  7527. if (isset($LEM->qcode2sgqa[$name]))
  7528. {
  7529. $name = $LEM->qcode2sgqa[$name];
  7530. }
  7531. else
  7532. {
  7533. return ''; // shouldn't happen
  7534. }
  7535. }
  7536. if (isset($_SESSION[$LEM->sessid][$name]))
  7537. {
  7538. $_result = $_SESSION[$LEM->sessid][$name];
  7539. }
  7540. else
  7541. {
  7542. $_result = (isset($LEM->knownVars[$name]['default']) ? $LEM->knownVars[$name]['default'] : 0);
  7543. }
  7544. switch($op)
  7545. {
  7546. case '=':
  7547. $_result = $value;
  7548. break;
  7549. case '*=':
  7550. $_result *= $value;
  7551. break;
  7552. case '/=':
  7553. $_result /= $value;
  7554. break;
  7555. case '+=':
  7556. $_result += $value;
  7557. break;
  7558. case '-=':
  7559. $_result -= $value;
  7560. break;
  7561. }
  7562. $_SESSION[$LEM->sessid][$name] = $_result;
  7563. $_type = $LEM->knownVars[$name]['type'];
  7564. $LEM->updatedValues[$name] = array(
  7565. 'type'=>$_type,
  7566. 'value'=>$_result,
  7567. );
  7568. return $_result;
  7569. }
  7570. }
  7571. /**
  7572. * Create HTML view of the survey, showing everything that uses EM
  7573. * @param <type> $sid
  7574. * @param <type> $gid
  7575. * @param <type> $qid
  7576. */
  7577. static public function ShowSurveyLogicFile($sid, $gid=NULL, $qid=NULL,$LEMdebugLevel=0,$assessments=false)
  7578. {
  7579. // Title
  7580. // Welcome
  7581. // G1, name, relevance, text
  7582. // *Q1, name [type], relevance [validation], text, help, default, help_msg
  7583. // SQ1, name [scale], relevance [validation], text
  7584. // A1, code, assessment_value, text
  7585. // End Message
  7586. $LEM =& LimeExpressionManager::singleton();
  7587. $aSurveyInfo=getSurveyInfo($sid);
  7588. $allErrors = array();
  7589. $warnings = 0;
  7590. $surveyOptions = array(
  7591. 'assessments'=>($aSurveyInfo['assessments']=='Y'),
  7592. 'hyperlinkSyntaxHighlighting'=>true,
  7593. );
  7594. $varNamesUsed = array(); // keeps track of whether variables have been declared
  7595. if (!is_null($qid))
  7596. {
  7597. $surveyMode='question';
  7598. LimeExpressionManager::StartSurvey($sid, 'question', $surveyOptions, false,$LEMdebugLevel);
  7599. $qseq = LimeExpressionManager::GetQuestionSeq($qid);
  7600. $moveResult = LimeExpressionManager::JumpTo($qseq+1,true,false,true);
  7601. }
  7602. else if (!is_null($gid))
  7603. {
  7604. $surveyMode='group';
  7605. LimeExpressionManager::StartSurvey($sid, 'group', $surveyOptions, false,$LEMdebugLevel);
  7606. $gseq = LimeExpressionManager::GetGroupSeq($gid);
  7607. $moveResult = LimeExpressionManager::JumpTo($gseq+1,true,false,true);
  7608. }
  7609. else
  7610. {
  7611. $surveyMode='survey';
  7612. LimeExpressionManager::StartSurvey($sid, 'survey', $surveyOptions, false,$LEMdebugLevel);
  7613. $moveResult = LimeExpressionManager::NavigateForwards();
  7614. }
  7615. $qtypes=getQuestionTypeList('','array');
  7616. if (is_null($moveResult) || is_null($LEM->currentQset) || count($LEM->currentQset) == 0) {
  7617. return array(
  7618. 'errors'=>1,
  7619. 'html'=>sprintf($LEM->gT('Invalid question - probably missing sub-questions or language-specific settings for language %s'),$_SESSION['LEMlang'])
  7620. );
  7621. }
  7622. $surveyname = templatereplace('{SURVEYNAME}',array('SURVEYNAME'=>$aSurveyInfo['surveyls_title']));
  7623. $out = '<div id="showlogicfilediv" ><H3>' . $LEM->gT('Logic File for Survey # ') . '[' . $LEM->sid . "]: $surveyname</H3>\n";
  7624. $out .= "<table id='logicfiletable'>";
  7625. if (is_null($gid) && is_null($qid))
  7626. {
  7627. $description = templatereplace('{SURVEYDESCRIPTION}', array('SURVEYDESCRIPTION'=>$aSurveyInfo['surveyls_description']));
  7628. $errClass = ($LEM->em->HasErrors() ? 'LEMerror' : '');
  7629. if ($description != '')
  7630. {
  7631. $out .= "<tr class='LEMgroup $errClass'><td colspan=2>" . $LEM->gT("Description:") . "</td><td colspan=2>" . $description . "</td></tr>";
  7632. }
  7633. $welcome = templatereplace('{WELCOME}', array('WELCOME'=>$aSurveyInfo['surveyls_welcometext']));
  7634. $errClass = ($LEM->em->HasErrors() ? 'LEMerror' : '');
  7635. if ($welcome != '')
  7636. {
  7637. $out .= "<tr class='LEMgroup $errClass'><td colspan=2>" . $LEM->gT("Welcome:") . "</td><td colspan=2>" . $welcome . "</td></tr>";
  7638. }
  7639. $endmsg = templatereplace('{ENDTEXT}', array('ENDTEXT'=>$aSurveyInfo['surveyls_endtext']));
  7640. $errClass = ($LEM->em->HasErrors() ? 'LEMerror' : '');
  7641. if ($endmsg != '')
  7642. {
  7643. $out .= "<tr class='LEMgroup $errClass'><td colspan=2>" . $LEM->gT("End message:") . "</td><td colspan=2>" . $endmsg . "</td></tr>";
  7644. }
  7645. $_linkreplace = templatereplace('{URL}', array('URL'=>$aSurveyInfo['surveyls_url']));
  7646. $errClass = ($LEM->em->HasErrors() ? 'LEMerror' : '');
  7647. if ($_linkreplace != '')
  7648. {
  7649. $out .= "<tr class='LEMgroup $errClass'><td colspan=2>" . $LEM->gT("End URL") . ":</td><td colspan=2>" . $_linkreplace . "</td></tr>";
  7650. }
  7651. }
  7652. $out .= "<tr><th>#</th><th>".$LEM->gT('Name [ID]')."</th><th>".$LEM->gT('Relevance [Validation] (Default)')."</th><th>".$LEM->gT('Text [Help] (Tip)')."</th></tr>\n";
  7653. $_gseq=-1;
  7654. foreach ($LEM->currentQset as $q) {
  7655. $gseq = $q['info']['gseq'];
  7656. $gid = $q['info']['gid'];
  7657. $qid = $q['info']['qid'];
  7658. $qseq = $q['info']['qseq'];
  7659. $errorCount=0;
  7660. //////
  7661. // SHOW GROUP-LEVEL INFO
  7662. //////
  7663. if ($gseq != $_gseq) {
  7664. $LEM->ParseResultCache=array(); // reset for each group so get proper color coding?
  7665. $_gseq = $gseq;
  7666. $ginfo = $LEM->gseq2info[$gseq];
  7667. $grelevance = '{' . (($ginfo['grelevance']=='') ? 1 : $ginfo['grelevance']) . '}';
  7668. $gtext = ((trim($ginfo['description']) == '') ? '&nbsp;' : $ginfo['description']);
  7669. $editlink = Yii::app()->getController()->createUrl('admin/survey/sa/view/surveyid/' . $LEM->sid . '/gid/' . $gid);
  7670. $groupRow = "<tr class='LEMgroup'>"
  7671. . "<td>G-$gseq</td>"
  7672. . "<td><b>".$ginfo['group_name']."</b><br />[<a target='_blank' href='$editlink'>GID ".$gid."</a>]</td>"
  7673. . "<td>".$grelevance."</td>"
  7674. . "<td>".$gtext."</td>"
  7675. . "</tr>\n";
  7676. $LEM->ProcessString($groupRow, $qid,NULL,false,1,1,false,false);
  7677. $out .= $LEM->GetLastPrettyPrintExpression();
  7678. if ($LEM->em->HasErrors()) {
  7679. ++$errorCount;
  7680. }
  7681. }
  7682. //////
  7683. // SHOW QUESTION-LEVEL INFO
  7684. //////
  7685. $mandatory = (($q['info']['mandatory']=='Y') ? "<span class='mandatory'>*</span>" : '');
  7686. $type = $q['info']['type'];
  7687. $typedesc = $qtypes[$type]['description'];
  7688. $sgqas = explode('|',$q['sgqa']);
  7689. if (count($sgqas) == 1 && !is_null($q['info']['default']))
  7690. {
  7691. $LEM->ProcessString($q['info']['default'], $qid,NULL,false,1,1,false,false);
  7692. $_default = $LEM->GetLastPrettyPrintExpression();
  7693. if ($LEM->em->HasErrors()) {
  7694. ++$errorCount;
  7695. }
  7696. $default = '<br />(' . $LEM->gT('Default:') . ' ' . $_default . ')';
  7697. }
  7698. else
  7699. {
  7700. $default = '';
  7701. }
  7702. $qtext = (($q['info']['qtext'] != '') ? $q['info']['qtext'] : '&nbsp');
  7703. $help = (($q['info']['help'] != '') ? '<hr/>[' . $LEM->gT("Help:") . ' ' . $q['info']['help'] . ']': '');
  7704. $prettyValidTip = (($q['prettyValidTip'] == '') ? '' : '<hr/>(' . $LEM->gT("Tip:") . ' ' . $q['prettyValidTip'] . ')');
  7705. //////
  7706. // SHOW QUESTION ATTRIBUTES THAT ARE PROCESSED BY EM
  7707. //////
  7708. $attrTable = '';
  7709. $attrs = (isset($LEM->qattr[$qid]) ? $LEM->qattr[$qid] : array());
  7710. if (isset($LEM->q2subqInfo[$qid]['preg']))
  7711. {
  7712. $attrs['regex_validation'] = $LEM->q2subqInfo[$qid]['preg'];
  7713. }
  7714. if (isset($LEM->questionSeq2relevance[$qseq]['other']))
  7715. {
  7716. $attrs['other'] = $LEM->questionSeq2relevance[$qseq]['other'];
  7717. }
  7718. if (count($attrs) > 0) {
  7719. $attrTable = "<table id='logicfileattributetable'><tr><th>" . $LEM->gT("Question attribute") . "</th><th>" . $LEM->gT("Value"). "</th></tr>\n";
  7720. $count=0;
  7721. foreach ($attrs as $key=>$value) {
  7722. if (is_null($value) || trim($value) == '') {
  7723. continue;
  7724. }
  7725. switch ($key)
  7726. {
  7727. // @todo: Rather compares the current attribute value to the defaults in the question attributes array to decide which ones should show (only the ones that are non-standard)
  7728. default:
  7729. case 'exclude_all_others':
  7730. case 'exclude_all_others_auto':
  7731. case 'hidden':
  7732. if ($value == false || $value == '0') {
  7733. $value = NULL; // so can skip this one - just using continue here doesn't work.
  7734. }
  7735. break;
  7736. case 'time_limit_action':
  7737. if ( $value == '1') {
  7738. $value = NULL; // so can skip this one - just using continue here doesn't work.
  7739. }
  7740. case 'relevance':
  7741. $value = NULL; // means an outdate database structure
  7742. break;
  7743. case 'array_filter':
  7744. case 'array_filter_exclude':
  7745. case 'code_filter':
  7746. case 'em_validation_q_tip':
  7747. case 'em_validation_sq_tip':
  7748. break;
  7749. case 'equals_num_value':
  7750. case 'em_validation_q':
  7751. case 'em_validation_sq':
  7752. case 'max_answers':
  7753. case 'max_num_value':
  7754. case 'max_num_value_n':
  7755. case 'min_answers':
  7756. case 'min_num_value':
  7757. case 'min_num_value_n':
  7758. case 'min_num_of_files':
  7759. case 'max_num_of_files':
  7760. case 'multiflexible_max':
  7761. case 'multiflexible_min':
  7762. $value = '{' . $value . '}';
  7763. break;
  7764. case 'other_replace_text':
  7765. case 'show_totals':
  7766. case 'regex_validation':
  7767. break;
  7768. case 'other':
  7769. if ($value == 'N') {
  7770. $value = NULL; // so can skip this one
  7771. }
  7772. break;
  7773. }
  7774. if (is_null($value)) {
  7775. continue; // since continuing from within a switch statement doesn't work
  7776. }
  7777. ++$count;
  7778. $attrTable .= "<tr><td>$key</td><td>$value</td></tr>\n";
  7779. }
  7780. $attrTable .= "</table>\n";
  7781. if ($count == 0) {
  7782. $attrTable = '';
  7783. }
  7784. }
  7785. $LEM->ProcessString($qtext . $help . $prettyValidTip . $attrTable, $qid,NULL,false,1,1,false,false);
  7786. $qdetails = $LEM->GetLastPrettyPrintExpression();
  7787. if ($LEM->em->HasErrors()) {
  7788. ++$errorCount;
  7789. }
  7790. //////
  7791. // SHOW RELEVANCE
  7792. //////
  7793. // Must parse Relevance this way, otherwise if try to first split expressions, regex equations won't work
  7794. $relevanceEqn = (($q['info']['relevance'] == '') ? 1 : $q['info']['relevance']);
  7795. if (!isset($LEM->ParseResultCache[$relevanceEqn]))
  7796. {
  7797. $result = $LEM->em->ProcessBooleanExpression($relevanceEqn, $gseq, $qseq);
  7798. $prettyPrint = $LEM->em->GetPrettyPrintString();
  7799. $hasErrors = $LEM->em->HasErrors();
  7800. $LEM->ParseResultCache[$relevanceEqn] = array(
  7801. 'result' => $result,
  7802. 'prettyprint' => $prettyPrint,
  7803. 'hasErrors' => $hasErrors,
  7804. );
  7805. }
  7806. $relevance = $LEM->ParseResultCache[$relevanceEqn]['prettyprint'];
  7807. if ($LEM->ParseResultCache[$relevanceEqn]['hasErrors']) {
  7808. ++$errorCount;
  7809. }
  7810. //////
  7811. // SHOW VALIDATION EQUATION
  7812. //////
  7813. // Must parse Validation this way so that regex (preg) works
  7814. $prettyValidEqn = '';
  7815. if ($q['prettyValidEqn'] != '') {
  7816. $validationEqn = $q['validEqn'];
  7817. if (!isset($LEM->ParseResultCache[$validationEqn]))
  7818. {
  7819. $result = $LEM->em->ProcessBooleanExpression($validationEqn, $gseq, $qseq);
  7820. $prettyPrint = $LEM->em->GetPrettyPrintString();
  7821. $hasErrors = $LEM->em->HasErrors();
  7822. $LEM->ParseResultCache[$validationEqn] = array(
  7823. 'result' => $result,
  7824. 'prettyprint' => $prettyPrint,
  7825. 'hasErrors' => $hasErrors,
  7826. );
  7827. }
  7828. $prettyValidEqn = '<hr/>(VALIDATION: ' . $LEM->ParseResultCache[$validationEqn]['prettyprint'] . ')';
  7829. if ($LEM->ParseResultCache[$validationEqn]['hasErrors']) {
  7830. ++$errorCount;
  7831. }
  7832. }
  7833. //////
  7834. // TEST VALIDITY OF ROOT VARIABLE NAME AND WHETHER HAS BEEN USED
  7835. //////
  7836. $rootVarName = $q['info']['rootVarName'];
  7837. $varNameErrorMsg = '';
  7838. $varNameError = NULL;
  7839. if (isset($varNamesUsed[$rootVarName]))
  7840. {
  7841. $varNameErrorMsg .= $LEM->gT('This variable name has already been used.');
  7842. }
  7843. else
  7844. {
  7845. $varNamesUsed[$rootVarName] = array(
  7846. 'gseq'=>$gseq,
  7847. 'qid'=>$qid
  7848. );
  7849. }
  7850. if (!preg_match('/^[_a-zA-Z][_0-9a-zA-Z]*$/', $rootVarName))
  7851. {
  7852. $varNameErrorMsg .= $LEM->gT('Starting in 1.92, variable names should only contain letters, numbers, and underscores; and may not start with a number. This variable name is deprecated.');
  7853. }
  7854. if ($varNameErrorMsg != '')
  7855. {
  7856. $varNameError = array (
  7857. 'message' => $varNameErrorMsg,
  7858. 'gseq' => $varNamesUsed[$rootVarName]['gseq'],
  7859. 'qid' => $varNamesUsed[$rootVarName]['qid'],
  7860. 'gid' => $gid,
  7861. );
  7862. if (!$LEM->sgqaNaming)
  7863. {
  7864. ++$errorCount;
  7865. }
  7866. else
  7867. {
  7868. ++$warnings;
  7869. }
  7870. }
  7871. //////
  7872. // SHOW ALL SUB-QUESTIONS
  7873. //////
  7874. $sqRows='';
  7875. $i=0;
  7876. $sawThis = array(); // array of rowdivids already seen so only show them once
  7877. foreach ($sgqas as $sgqa)
  7878. {
  7879. if ($LEM->knownVars[$sgqa]['qcode'] == $rootVarName) {
  7880. continue; // so don't show the main question as a sub-question too
  7881. }
  7882. $rowdivid=$sgqa;
  7883. $varName=$LEM->knownVars[$sgqa]['qcode'];
  7884. switch ($q['info']['type'])
  7885. {
  7886. case '1':
  7887. if (preg_match('/#1$/',$sgqa)) {
  7888. $rowdivid = NULL; // so that doesn't show same message for second scale
  7889. }
  7890. else {
  7891. $rowdivid = substr($sgqa,0,-2); // strip suffix
  7892. $varName = substr($LEM->knownVars[$sgqa]['qcode'],0,-2);
  7893. }
  7894. break;
  7895. case 'P':
  7896. if (preg_match('/comment$/',$sgqa)) {
  7897. $rowdivid = NULL;
  7898. }
  7899. break;
  7900. case ':':
  7901. case ';':
  7902. $_rowdivid = $LEM->knownVars[$sgqa]['rowdivid'];
  7903. if (isset($sawThis[$qid . '~' . $_rowdivid])) {
  7904. $rowdivid = NULL; // so don't show again
  7905. }
  7906. else {
  7907. $sawThis[$qid . '~' . $_rowdivid] = true;
  7908. $rowdivid = $_rowdivid;
  7909. $sgqa_len = strlen($sid . 'X'. $gid . 'X' . $qid);
  7910. $varName = $rootVarName . '_' . substr($_rowdivid,$sgqa_len);
  7911. }
  7912. }
  7913. if (is_null($rowdivid)) {
  7914. continue;
  7915. }
  7916. ++$i;
  7917. $subQeqn = '&nbsp;';
  7918. if (isset($LEM->subQrelInfo[$qid][$rowdivid]))
  7919. {
  7920. $sq = $LEM->subQrelInfo[$qid][$rowdivid];
  7921. $subQeqn = $sq['prettyPrintEqn']; // {' . $sq['eqn'] . '}'; // $sq['prettyPrintEqn'];
  7922. if ($sq['hasErrors']) {
  7923. ++$errorCount;
  7924. }
  7925. }
  7926. $sgqaInfo = $LEM->knownVars[$sgqa];
  7927. $subqText = $sgqaInfo['subqtext'];
  7928. if (isset($sgqaInfo['default']) && $sgqaInfo['default'] !== '')
  7929. {
  7930. $LEM->ProcessString($sgqaInfo['default'], $qid,NULL,false,1,1,false,false);
  7931. $_default = $LEM->GetLastPrettyPrintExpression();
  7932. if ($LEM->em->HasErrors()) {
  7933. ++$errorCount;
  7934. }
  7935. $subQeqn .= '<br />(' . $LEM->gT('Default:') . ' ' . $_default . ')';
  7936. }
  7937. $sqRows .= "<tr class='LEMsubq'>"
  7938. . "<td>SQ-$i</td>"
  7939. . "<td><b>" . $varName . "</b></td>"
  7940. . "<td>$subQeqn</td>"
  7941. . "<td>" .$subqText . "</td>"
  7942. . "</tr>";
  7943. }
  7944. $LEM->ProcessString($sqRows, $qid,NULL,false,1,1,false,false);
  7945. $sqRows = $LEM->GetLastPrettyPrintExpression();
  7946. if ($LEM->em->HasErrors()) {
  7947. ++$errorCount;
  7948. }
  7949. //////
  7950. // SHOW ANSWER OPTIONS FOR ENUMERATED LISTS, AND FOR MULTIFLEXI
  7951. //////
  7952. $answerRows='';
  7953. if (isset($LEM->qans[$qid]) || isset($LEM->multiflexiAnswers[$qid]))
  7954. {
  7955. $_scale=-1;
  7956. if (isset($LEM->multiflexiAnswers[$qid])) {
  7957. $ansList = $LEM->multiflexiAnswers[$qid];
  7958. }
  7959. else {
  7960. $ansList = $LEM->qans[$qid];
  7961. }
  7962. foreach ($ansList as $ans=>$value)
  7963. {
  7964. $ansInfo = explode('~',$ans);
  7965. $valParts = explode('|',$value);
  7966. $valInfo[0] = array_shift($valParts);
  7967. $valInfo[1] = implode('|',$valParts);
  7968. if ($_scale != $ansInfo[0]) {
  7969. $i=1;
  7970. $_scale = $ansInfo[0];
  7971. }
  7972. $subQeqn = '';
  7973. $rowdivid = $sgqas[0] . $ansInfo[1];
  7974. if ($q['info']['type'] == 'R')
  7975. {
  7976. $rowdivid = $LEM->sid . 'X' . $gid . 'X' . $qid . $ansInfo[1];
  7977. }
  7978. if (isset($LEM->subQrelInfo[$qid][$rowdivid]))
  7979. {
  7980. $sq = $LEM->subQrelInfo[$qid][$rowdivid];
  7981. $subQeqn = ' ' . $sq['prettyPrintEqn'];
  7982. if ($sq['hasErrors']) {
  7983. ++$errorCount;
  7984. }
  7985. }
  7986. $answerRows .= "<tr class='LEManswer'>"
  7987. . "<td>A[" . $ansInfo[0] . "]-" . $i++ . "</td>"
  7988. . "<td><b>" . $ansInfo[1]. "</b></td>"
  7989. . "<td>[VALUE: " . $valInfo[0] . "]".$subQeqn."</td>"
  7990. . "<td>" . $valInfo[1] . "</td>"
  7991. . "</tr>\n";
  7992. }
  7993. $LEM->ProcessString($answerRows, $qid,NULL,false,1,1,false,false);
  7994. $answerRows = $LEM->GetLastPrettyPrintExpression();
  7995. if ($LEM->em->HasErrors()) {
  7996. ++$errorCount;
  7997. }
  7998. }
  7999. //////
  8000. // FINALLY, SHOW THE QUESTION ROW(S), COLOR-CODING QUESTIONS THAT CONTAIN ERRORS
  8001. //////
  8002. $errclass = ($errorCount > 0) ? "class='LEMerror' title='" . sprintf($LEM->ngT("This question has at least %s error.","This question has at least %s errors.",$errorCount), $errorCount) . "'" : '';
  8003. $questionRow = "<tr class='LEMquestion'>"
  8004. . "<td $errclass>Q-" . $q['info']['qseq'] . "</td>"
  8005. . "<td><b>" . $mandatory;
  8006. if ($varNameErrorMsg == '')
  8007. {
  8008. $questionRow .= $rootVarName;
  8009. }
  8010. else
  8011. {
  8012. $editlink = Yii::app()->getController()->createUrl('admin/survey/sa/view/surveyid/' . $LEM->sid . '/gid/' . $varNameError['gid'] . '/qid/' . $varNameError['qid']);
  8013. $questionRow .= "<span class='highlighterror' title='" . $varNameError['message'] . "' "
  8014. . "onclick='window.open(\"$editlink\",\"_blank\")'>"
  8015. . $rootVarName . "</span>";
  8016. }
  8017. $editlink = Yii::app()->getController()->createUrl('admin/survey/sa/view/surveyid/' . $sid . '/gid/' . $gid . '/qid/' . $qid);
  8018. $questionRow .= "</b><br />[<a target='_blank' href='$editlink'>QID $qid</a>]<br/>$typedesc [$type]</td>"
  8019. . "<td>" . $relevance . $prettyValidEqn . $default . "</td>"
  8020. . "<td>" . $qdetails . "</td>"
  8021. . "</tr>\n";
  8022. $out .= $questionRow;
  8023. $out .= $sqRows;
  8024. $out .= $answerRows;
  8025. if ($errorCount > 0) {
  8026. $allErrors[$gid . '~' . $qid] = $errorCount;
  8027. }
  8028. }
  8029. $out .= "</table>";
  8030. LimeExpressionManager::FinishProcessingPage();
  8031. if (($LEMdebugLevel & LEM_DEBUG_TIMING) == LEM_DEBUG_TIMING) {
  8032. $out .= LimeExpressionManager::GetDebugTimingMessage();
  8033. }
  8034. if (count($allErrors) > 0) {
  8035. $out = "<p class='LEMerror'>". sprintf($LEM->ngT("%s question contains errors that need to be corrected","%s questions contain errors that need to be corrected",count($allErrors)), count($allErrors)) . "</p>\n" . $out;
  8036. }
  8037. else {
  8038. switch ($surveyMode)
  8039. {
  8040. case 'survey':
  8041. $message = $LEM->gT('No syntax errors detected in this survey');
  8042. break;
  8043. case 'group':
  8044. $message = $LEM->gT('This group, by itself, does not contain any syntax errors');
  8045. break;
  8046. case 'question':
  8047. $message = $LEM->gT('This question, by itself, does not contain any syntax errors');
  8048. break;
  8049. }
  8050. $out = "<p class='LEMheading'>$message</p>\n" . $out."</div>";
  8051. }
  8052. return array(
  8053. 'errors'=>$allErrors,
  8054. 'html'=>$out
  8055. );
  8056. }
  8057. /**
  8058. * TSV survey definition in format readable by TSVSurveyImport
  8059. * one line each per group, question, sub-question, and answer
  8060. * does not use SGQA naming at all.
  8061. * @param type $sid
  8062. * @return type
  8063. */
  8064. static public function &TSVSurveyExport($sid)
  8065. {
  8066. $fields = array(
  8067. 'class',
  8068. 'type/scale',
  8069. 'name',
  8070. 'relevance',
  8071. 'text',
  8072. 'help',
  8073. 'language',
  8074. 'validation',
  8075. 'mandatory',
  8076. 'other',
  8077. 'default',
  8078. 'same_default',
  8079. // Advanced question attributes
  8080. 'allowed_filetypes',
  8081. 'alphasort',
  8082. 'answer_width',
  8083. 'array_filter',
  8084. 'array_filter_exclude',
  8085. 'assessment_value',
  8086. 'category_separator',
  8087. 'display_columns',
  8088. 'display_rows',
  8089. 'dropdown_dates_year_min',
  8090. 'dropdown_dates',
  8091. 'dropdown_dates_year_max',
  8092. 'dropdown_prefix',
  8093. 'dropdown_prepostfix',
  8094. 'dropdown_separators',
  8095. 'dropdown_size',
  8096. 'dualscale_headerA',
  8097. 'dualscale_headerB',
  8098. 'em_validation_q_tip',
  8099. 'em_validation_q',
  8100. 'em_validation_sq',
  8101. 'em_validation_sq_tip',
  8102. 'equals_num_value',
  8103. 'exclude_all_others',
  8104. 'exclude_all_others_auto',
  8105. 'hidden',
  8106. 'hide_tip',
  8107. 'input_boxes',
  8108. 'location_city',
  8109. 'location_country',
  8110. 'location_defaultcoordinates',
  8111. 'location_mapheight',
  8112. 'location_mapservice',
  8113. 'location_mapwidth',
  8114. 'location_mapzoom',
  8115. 'location_nodefaultfromip',
  8116. 'location_postal',
  8117. 'location_state',
  8118. 'max_answers',
  8119. 'max_filesize',
  8120. 'max_num_of_files',
  8121. 'max_num_value',
  8122. 'max_num_value_n',
  8123. 'maximum_chars',
  8124. 'min_answers',
  8125. 'min_num_of_files',
  8126. 'min_num_value',
  8127. 'min_num_value_n',
  8128. 'multiflexible_checkbox',
  8129. 'multiflexible_max',
  8130. 'multiflexible_min',
  8131. 'multiflexible_step',
  8132. 'num_value_int_only',
  8133. 'numbers_only',
  8134. 'other_comment_mandatory',
  8135. 'other_numbers_only',
  8136. 'other_replace_text',
  8137. 'page_break',
  8138. 'prefix',
  8139. 'public_statistics',
  8140. 'random_group',
  8141. 'random_order',
  8142. 'reverse',
  8143. 'scale_export',
  8144. 'show_comment',
  8145. 'show_grand_total',
  8146. 'show_title',
  8147. 'show_totals',
  8148. 'slider_accuracy',
  8149. 'slider_default',
  8150. 'slider_layout',
  8151. 'slider_max',
  8152. 'slider_middlestart',
  8153. 'slider_min',
  8154. 'slider_rating',
  8155. 'slider_separator',
  8156. 'slider_showminmax',
  8157. 'suffix',
  8158. 'text_input_width',
  8159. 'time_limit_action',
  8160. 'time_limit_countdown_message',
  8161. 'time_limit_disable_next',
  8162. 'time_limit_disable_prev',
  8163. 'time_limit_message',
  8164. 'time_limit_message_delay',
  8165. 'time_limit_message_style',
  8166. 'time_limit_timer_style',
  8167. 'time_limit_warning_2_display_time',
  8168. 'time_limit_warning_2_message',
  8169. 'time_limit_warning_2_style',
  8170. 'time_limit_warning_2',
  8171. 'time_limit_warning',
  8172. 'time_limit',
  8173. 'time_limit_warning_display_time',
  8174. 'time_limit_warning_message',
  8175. 'time_limit_warning_style',
  8176. 'use_dropdown',
  8177. );
  8178. $rows = array();
  8179. $primarylang='en';
  8180. $otherlangs='';
  8181. $langs = array();
  8182. // Export survey-level information
  8183. $query = "select * from {{surveys}} where sid = " . $sid;
  8184. $data = dbExecuteAssoc($query);
  8185. foreach ($data->readAll() as $r)
  8186. {
  8187. foreach ($r as $key=>$value)
  8188. {
  8189. if ($value != '')
  8190. {
  8191. $row['class'] = 'S';
  8192. $row['name'] = $key;
  8193. $row['text'] = $value;
  8194. $rows[] = $row;
  8195. }
  8196. if ($key=='language')
  8197. {
  8198. $primarylang = $value;
  8199. }
  8200. if ($key=='additional_languages')
  8201. {
  8202. $otherlangs = $value;
  8203. }
  8204. }
  8205. }
  8206. $langs = explode(' ',$primarylang . ' ' . $otherlangs);
  8207. $langs = array_unique($langs);
  8208. // Export survey language settings
  8209. $query = "select * from {{surveys_languagesettings}} where surveyls_survey_id = " . $sid;
  8210. $data = dbExecuteAssoc($query);
  8211. foreach ($data->readAll() as $r)
  8212. {
  8213. $_lang = $r['surveyls_language'];
  8214. foreach ($r as $key=>$value)
  8215. {
  8216. if ($value != '' && $key != 'surveyls_language' && $key != 'surveyls_survey_id')
  8217. {
  8218. $row['class'] = 'SL';
  8219. $row['name'] = $key;
  8220. $row['text'] = $value;
  8221. $row['language'] = $_lang;
  8222. $rows[] = $row;
  8223. }
  8224. }
  8225. }
  8226. $surveyinfo = getSurveyInfo($sid);
  8227. $assessments = false;
  8228. if (isset($surveyinfo['assessments']) && $surveyinfo['assessments']=='Y')
  8229. {
  8230. $assessments = true;
  8231. }
  8232. foreach($langs as $lang)
  8233. {
  8234. if (trim($lang) == '')
  8235. {
  8236. continue;
  8237. }
  8238. SetSurveyLanguage($sid,$lang);
  8239. LimeExpressionManager::StartSurvey($sid, 'survey', array('sgqaNaming'=>'N','assessments'=>$assessments), true);
  8240. $moveResult = LimeExpressionManager::NavigateForwards();
  8241. $LEM =& LimeExpressionManager::singleton();
  8242. if (is_null($moveResult) || is_null($LEM->currentQset) || count($LEM->currentQset) == 0) {
  8243. continue;
  8244. }
  8245. $_gseq=-1;
  8246. foreach ($LEM->currentQset as $q) {
  8247. $gseq = $q['info']['gseq'];
  8248. $gid = $q['info']['gid'];
  8249. $qid = $q['info']['qid'];
  8250. //////
  8251. // SHOW GROUP-LEVEL INFO
  8252. //////
  8253. if ($gseq != $_gseq) {
  8254. $_gseq = $gseq;
  8255. $ginfo = $LEM->gseq2info[$gseq];
  8256. // if relevance equation is using SGQA coding, convert to qcoding
  8257. $grelevance = (($ginfo['grelevance']=='') ? 1 : $ginfo['grelevance']);
  8258. $LEM->em->ProcessBooleanExpression($grelevance, $gseq, 0); // $qseq
  8259. $grelevance = trim(strip_tags($LEM->em->GetPrettyPrintString()));
  8260. $gtext = ((trim($ginfo['description']) == '') ? '' : $ginfo['description']);
  8261. $row = array();
  8262. $row['class'] = 'G';
  8263. $row['name'] = $ginfo['group_name'];
  8264. $row['relevance'] = $grelevance;
  8265. $row['text'] = $gtext;
  8266. $row['language'] = $lang;
  8267. $rows[] = $row;
  8268. }
  8269. //////
  8270. // SHOW QUESTION-LEVEL INFO
  8271. //////
  8272. $row = array();
  8273. $mandatory = (($q['info']['mandatory']=='Y') ? 'Y' : '');
  8274. $type = $q['info']['type'];
  8275. $sgqas = explode('|',$q['sgqa']);
  8276. if (count($sgqas) == 1 && !is_null($q['info']['default']))
  8277. {
  8278. $default = $q['info']['default'];
  8279. }
  8280. else
  8281. {
  8282. $default = '';
  8283. }
  8284. $qtext = (($q['info']['qtext'] != '') ? $q['info']['qtext'] : '');
  8285. $help = (($q['info']['help'] != '') ? $q['info']['help']: '');
  8286. //////
  8287. // SHOW QUESTION ATTRIBUTES THAT ARE PROCESSED BY EM
  8288. //////
  8289. if (isset($LEM->qattr[$qid]) && count($LEM->qattr[$qid]) > 0) {
  8290. foreach ($LEM->qattr[$qid] as $key=>$value) {
  8291. if (is_null($value) || trim($value) == '') {
  8292. continue;
  8293. }
  8294. switch ($key)
  8295. {
  8296. default:
  8297. case 'exclude_all_others':
  8298. case 'exclude_all_others_auto':
  8299. case 'hidden':
  8300. if ($value == false || $value == '0') {
  8301. $value = NULL; // so can skip this one - just using continue here doesn't work.
  8302. }
  8303. break;
  8304. case 'relevance':
  8305. $value = NULL; // means an outdate database structure
  8306. break;
  8307. }
  8308. if (is_null($value) || trim($value) == '') {
  8309. continue; // since continuing from within a switch statement doesn't work
  8310. }
  8311. $row[$key] = $value;
  8312. }
  8313. }
  8314. // if relevance equation is using SGQA coding, convert to qcoding
  8315. $relevanceEqn = (($q['info']['relevance'] == '') ? 1 : $q['info']['relevance']);
  8316. $LEM->em->ProcessBooleanExpression($relevanceEqn, $gseq, $q['info']['qseq']); // $qseq
  8317. $relevanceEqn = trim(strip_tags($LEM->em->GetPrettyPrintString()));
  8318. $rootVarName = $q['info']['rootVarName'];
  8319. $preg = '';
  8320. if (isset($LEM->q2subqInfo[$q['info']['qid']]['preg']))
  8321. {
  8322. $preg = $LEM->q2subqInfo[$q['info']['qid']]['preg'];
  8323. if (is_null($preg))
  8324. {
  8325. $preg = '';
  8326. }
  8327. }
  8328. $row['class'] = 'Q';
  8329. $row['type/scale'] = $type;
  8330. $row['name'] = $rootVarName;
  8331. $row['relevance'] = $relevanceEqn;
  8332. $row['text'] = $qtext;
  8333. $row['help'] = $help;
  8334. $row['language'] = $lang;
  8335. $row['validation'] = $preg;
  8336. $row['mandatory'] = $mandatory;
  8337. $row['other'] = $q['info']['other'];
  8338. $row['default'] = $default;
  8339. $row['same_default'] = 1; // TODO - need this: $q['info']['same_default'];
  8340. $rows[] = $row;
  8341. //////
  8342. // SHOW ALL SUB-QUESTIONS
  8343. //////
  8344. $sawThis = array(); // array of rowdivids already seen so only show them once
  8345. foreach ($sgqas as $sgqa)
  8346. {
  8347. if ($LEM->knownVars[$sgqa]['qcode'] == $rootVarName) {
  8348. continue; // so don't show the main question as a sub-question too
  8349. }
  8350. $rowdivid=$sgqa;
  8351. $varName=$LEM->knownVars[$sgqa]['qcode'];
  8352. switch ($q['info']['type'])
  8353. {
  8354. case '1':
  8355. if (preg_match('/#1$/',$sgqa)) {
  8356. $rowdivid = NULL; // so that doesn't show same message for second scale
  8357. }
  8358. else {
  8359. $rowdivid = substr($sgqa,0,-2); // strip suffix
  8360. $varName = substr($LEM->knownVars[$sgqa]['qcode'],0,-2);
  8361. }
  8362. break;
  8363. case 'P':
  8364. if (preg_match('/comment$/',$sgqa)) {
  8365. $rowdivid = NULL;
  8366. }
  8367. break;
  8368. case ':':
  8369. case ';':
  8370. $_rowdivid = $LEM->knownVars[$sgqa]['rowdivid'];
  8371. if (isset($sawThis[$qid . '~' . $_rowdivid])) {
  8372. $rowdivid = NULL; // so don't show again
  8373. }
  8374. else {
  8375. $sawThis[$qid . '~' . $_rowdivid] = true;
  8376. $rowdivid = $_rowdivid;
  8377. $sgqa_len = strlen($sid . 'X'. $gid . 'X' . $qid);
  8378. $varName = $rootVarName . '_' . substr($_rowdivid,$sgqa_len);
  8379. }
  8380. break;
  8381. }
  8382. if (is_null($rowdivid)) {
  8383. continue;
  8384. }
  8385. $sgqaInfo = $LEM->knownVars[$sgqa];
  8386. $subqText = $sgqaInfo['subqtext'];
  8387. if (isset($sgqaInfo['default']))
  8388. {
  8389. $default = $sgqaInfo['default'];
  8390. }
  8391. else
  8392. {
  8393. $default = '';
  8394. }
  8395. $row = array();
  8396. $row['class'] = 'SQ';
  8397. $row['type/scale'] = 0;
  8398. $row['name'] = substr($varName,strlen($rootVarName)+1);
  8399. $row['text'] = $subqText;
  8400. $row['language'] = $lang;
  8401. $row['default'] = $default;
  8402. $rows[] = $row;
  8403. }
  8404. //////
  8405. // SHOW ANSWER OPTIONS FOR ENUMERATED LISTS, AND FOR MULTIFLEXI
  8406. //////
  8407. if (isset($LEM->qans[$qid]) || isset($LEM->multiflexiAnswers[$qid]))
  8408. {
  8409. $_scale=-1;
  8410. if (isset($LEM->multiflexiAnswers[$qid])) {
  8411. $ansList = $LEM->multiflexiAnswers[$qid];
  8412. }
  8413. else {
  8414. $ansList = $LEM->qans[$qid];
  8415. }
  8416. foreach ($ansList as $ans=>$value)
  8417. {
  8418. $ansInfo = explode('~',$ans);
  8419. $valParts = explode('|',$value);
  8420. $valInfo[0] = array_shift($valParts);
  8421. $valInfo[1] = implode('|',$valParts);
  8422. if ($_scale != $ansInfo[0]) {
  8423. $_scale = $ansInfo[0];
  8424. }
  8425. $row = array();
  8426. if ($type == ':' || $type == ';')
  8427. {
  8428. $row['class'] = 'SQ';
  8429. }
  8430. else
  8431. {
  8432. $row['class'] = 'A';
  8433. }
  8434. $row['type/scale'] = $_scale;
  8435. $row['name'] = $ansInfo[1];
  8436. $row['relevance'] = $valInfo[0]; // TODO - true? - what if it isn't an assessment value?
  8437. $row['text'] = $valInfo[1];
  8438. $row['language'] = $lang;
  8439. $rows[] = $row;
  8440. }
  8441. }
  8442. }
  8443. }
  8444. // Now generate the array out output data
  8445. $out = array();
  8446. $out[] = $fields;
  8447. foreach ($rows as $row)
  8448. {
  8449. $tsv = array();
  8450. foreach ($fields as $field)
  8451. {
  8452. $val = (isset($row[$field]) ? $row[$field] : '');
  8453. $tsv[] = $val;
  8454. }
  8455. $out[] = $tsv;
  8456. }
  8457. return $out;
  8458. }
  8459. /**
  8460. * Returns the survey ID of the EM singleton
  8461. */
  8462. public static function getLEMsurveyId() {
  8463. $LEM =& LimeExpressionManager::singleton();
  8464. return $LEM->sid;
  8465. }
  8466. }
  8467. /**
  8468. * Used by usort() to order $this->questionSeq2relevance in proper order
  8469. * @param <type> $a
  8470. * @param <type> $b
  8471. * @return <type>
  8472. */
  8473. function cmpQuestionSeq($a, $b)
  8474. {
  8475. if (is_null($a['qseq'])) {
  8476. if (is_null($b['qseq'])) {
  8477. return 0;
  8478. }
  8479. return 1;
  8480. }
  8481. if (is_null($b['qseq'])) {
  8482. return -1;
  8483. }
  8484. if ($a['qseq'] == $b['qseq']) {
  8485. return 0;
  8486. }
  8487. return ($a['qseq'] < $b['qseq']) ? -1 : 1;
  8488. }
  8489. ?>