PageRenderTime 59ms CodeModel.GetById 15ms RepoModel.GetById 1ms 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

Large files files are truncated, but you can click here to view the full file

  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!=''

Large files files are truncated, but you can click here to view the full file