PageRenderTime 50ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/cms/modules/quiz/simplequiz.php

https://github.com/akash6190/pragyan
PHP | 841 lines | 599 code | 94 blank | 148 comment | 135 complexity | fb5da0b6fd86a32fdea5696ccac7fa76 MD5 | raw file
  1. <?php
  2. if(!defined('__PRAGYAN_CMS'))
  3. {
  4. header($_SERVER['SERVER_PROTOCOL'].' 403 Forbidden');
  5. echo "<h1>403 Forbidden<h1><h4>You are not authorized to access the page.</h4>";
  6. echo '<hr/>'.$_SERVER['SERVER_SIGNATURE'];
  7. exit(1);
  8. }
  9. /*
  10. * Created on Jan 15, 2009
  11. */
  12. // SSO: optAnswer{SectionId}_{QuestionId} => value (OptionId)
  13. // MSO: chkAnswer{SectionId}_{QuestionId}_{OptionId}
  14. // Subj: txtAnswer{SectionId}_{QuestionId}
  15. define('QUIZ_COMPLETED', 1);
  16. define('QUIZ_SUBMISSIONFAILED', false);
  17. define('QUIZ_SUBMISSIONSUCCESSFUL', true);
  18. define('QUIZ_TIMEOUT_ERRORMSG', 'You have run out of time for this quiz. Your test will be evaluated only for the answers you have submitted previously.');
  19. define('QUIZ_SECTION_TIMEOUT_ERRORMSG', 'You have run out of time for this section. Only the answers that you have submitted previously will be evaluated. You can still view sections for which you have time left from the <a href="./+view">Quiz main page</a>.');
  20. class SimpleQuiz implements IQuiz {
  21. private $quizId;
  22. private $quizRow;
  23. public function __construct($quizId) {
  24. $this->quizId = $quizId;
  25. $this->quizRow = getQuizRow($quizId);
  26. }
  27. /**
  28. * function getPropertiesForm:
  29. * will be called from quiz edit, here no quiz specific properties
  30. */
  31. public function getPropertiesForm($dataSource) {
  32. return 'No quiz specific properties.';
  33. }
  34. public function submitPropertiesForm() {
  35. return true;
  36. }
  37. /**
  38. * function getSectionStartForm:
  39. * return HTML Section start form
  40. * form with a button and a hidden field specifying section id
  41. */
  42. private function getSectionStartForm($sectionId) {
  43. return <<<SECTIONSTARTFORM
  44. <form name="sectionstartform" method="POST" action="./+view" style="padding:0;margin:0;display:inline">
  45. <input type="hidden" name="hdnSectionId" id="hdnSectionId" value="$sectionId" />
  46. <input type="submit" name="btnStartSection" id="btnStartSection" value="Start" />
  47. </form>
  48. SECTIONSTARTFORM;
  49. }
  50. /**
  51. * function getFrontPage:
  52. * if random access is set, front page displays list of available sections and lets user select section
  53. */
  54. public function getFrontPage($userId) {
  55. $frontPage = "<h2>{$this->quizRow['quiz_title']}</h2>\n";
  56. $frontPage .= "<div class=\"quiz_headertext\">{$this->quizRow['quiz_headertext']}</div><br /><br />\n";
  57. if ($this->quizRow['quiz_allowsectionrandomaccess']) {
  58. $sectionList = getSectionList($this->quizId);
  59. for ($i = 0; $i < count($sectionList); ++$i) {
  60. $frontPage .= '<strong>' . $sectionList[$i]['quiz_sectiontitle'] . '</strong> ';
  61. $attemptRow = getAttemptRow($this->quizId, $sectionList[$i]['quiz_sectionid'], $userId);
  62. if (!$attemptRow || is_null($attemptRow['quiz_attemptstarttime'])) {
  63. // User hasn't started this section yet.
  64. $frontPage .= $this->getSectionStartForm($sectionList[$i]['quiz_sectionid']);
  65. }
  66. elseif (is_null($attemptRow['quiz_submissiontime'])) {
  67. // User hasn't finished this section yet.
  68. $frontPage .= ' <a href="./+view&sectionid=' . $sectionList[$i]['quiz_sectionid'] . '">Go to questions</a>';
  69. }
  70. else {
  71. // User has finished the section already.
  72. $frontPage .= " Section Completed.";
  73. }
  74. $frontPage .= '<br /><br />';
  75. }
  76. }
  77. else {
  78. $frontPage .= <<<QUIZSTARTFORM
  79. <form name="quizstartform" method="POST" action="./+view" style="padding:0;margin:0;display:inline">
  80. <input type="submit" name="btnStartQuiz" id="btnStartQuiz" value="Start" />
  81. </form>
  82. QUIZSTARTFORM;
  83. }
  84. return $frontPage;
  85. }
  86. /**
  87. * function getQuizPage:
  88. * Retrieves the next page for the user.
  89. * Use this function from outside the class.
  90. * @param Integer $userId User ID.
  91. * @return String HTML for the next page.
  92. */
  93. public function getQuizPage($userId) {
  94. if ($this->checkQuizCompleted($userId)) {
  95. displayinfo('You seem to have completed this quiz already. You can only take this quiz once.');
  96. return '';
  97. }
  98. if ($this->quizRow['quiz_allowsectionrandomaccess']) {
  99. // if btnStartSection and hdnSectionId are set
  100. if (isset($_POST['btnStartSection']) && $this->isValidId($_POST['hdnSectionId']) && sectionBelongsToQuiz($this->quizId, $_POST['hdnSectionId']))
  101. $sectionId = intval($_POST['hdnSectionId']);
  102. elseif (isset($_GET['sectionid']) && $this->isValidId($_GET['sectionid']))
  103. $sectionId = intval($_GET['sectionid']);
  104. if (!isset($sectionId))
  105. return $this->getFrontPage($userId);
  106. $attemptRow = getAttemptRow($this->quizId, $sectionId, $userId);
  107. $sectionStarted = $attemptRow ? true : false;
  108. $sectionCompleted = !is_null($attemptRow['quiz_submissiontime']);
  109. if (!$sectionStarted) {
  110. if (!isset($_POST['btnStartSection'])) {
  111. displayerror('Error. You have not started this section yet. Please go to the quiz main page, and click on the Start Section button to view this section.');
  112. return '';
  113. }
  114. if (!startSection($this->quizId, $sectionId, $userId))
  115. return '';
  116. }
  117. elseif ($sectionCompleted) {
  118. displayinfo("You have completed this section.");
  119. return '';
  120. }
  121. if (isset($_POST['btnSubmit'])) {
  122. if ($this->submitQuizPage($userId) === true) {
  123. if ($this->markSectionCompleted($userId, $sectionId)) {
  124. // This section has been completed. See if the quiz also got completed
  125. if ($this->checkQuizCompleted($userId)) {
  126. return $this->quizRow['quiz_submittext'];
  127. }
  128. else {
  129. displayinfo('You have completed this section. You can move to another section.');
  130. return $this->getFrontPage($userId);
  131. }
  132. }
  133. else
  134. displayinfo('Your previous page was submitted successfully.');
  135. }
  136. }
  137. // TODO: Put in time check here
  138. if ($this->checkUserTimedOut($userId)) {
  139. displayerror(QUIZ_TIMEOUT_ERRORMSG);
  140. $this->forceQuizCompleted($userId);
  141. return '';
  142. }
  143. elseif ($this->checkUserTimedOut($userId, $sectionId)) {
  144. displayerror(QUIZ_SECTION_TIMEOUT_ERRORMSG);
  145. $this->forceQuizCompleted($userId, $sectionId);
  146. return '';
  147. }
  148. return $this->formatNextPage($userId, $sectionId);
  149. }
  150. else {
  151. // if quiz is already started, show next page
  152. // else, see if btnStartQuiz is set, if yes, mark quiz as started, and show next page
  153. // to mark a user's quiz as started, we insert one entry for each section in the quiz into user_attempts
  154. // to see if the user's quiz has been started, we see if there is a row in user_attempts with section id 1
  155. $minSectionId = getFirstSectionId($this->quizId);
  156. $attemptRow = getAttemptRow($this->quizId, $minSectionId, $userId);
  157. if (!$attemptRow) {
  158. if (!isset($_POST['btnStartQuiz']))
  159. return $this->getFrontPage($userId);
  160. // ok, btnStartQuiz was set, and the quiz wasn't started already,
  161. // start it by inserting a row for each section in the quiz into quiz_userattempts.
  162. $attemptQuery = "INSERT INTO `quiz_userattempts`(`page_modulecomponentid`, `quiz_sectionid`, `user_id`, `quiz_attemptstarttime`) " .
  163. "SELECT {$this->quizId}, `quiz_sectionid`, $userId, NOW() FROM `quiz_sections` WHERE `page_modulecomponentid` = '{$this->quizId}'";
  164. if (!mysql_query($attemptQuery)) {
  165. displayerror('Database Error. Could not update quiz information.');
  166. return '';
  167. }
  168. }
  169. if (isset($_POST['btnSubmit'])) {
  170. if ($this->submitQuizPage($userId) == true) {
  171. if ($this->markSectionCompleted($userId, -1)) {
  172. if ($this->checkQuizCompleted($userId))
  173. return $this->quizRow['quiz_submittext'];
  174. }
  175. else
  176. displayinfo('Your previous page was submitted successfully.');
  177. }
  178. }
  179. // TODO: Put in time check here
  180. if ($this->checkUserTimedOut($userId)) {
  181. displayerror(QUIZ_TIMEOUT_ERRORMSG);
  182. $this->forceQuizCompleted($userId);
  183. return '';
  184. }
  185. return $this->formatNextPage($userId);
  186. }
  187. }
  188. /**
  189. * function submitQuizPage:
  190. * Submits a page worth of questions.
  191. * @param Integer $userId User ID of the user taking the quiz.
  192. * @return Boolean True indicating successful submission, and false indicating errors.
  193. */
  194. public function submitQuizPage($userId) {
  195. // get all the questions that have been shown to the user
  196. // get the submitted answer for all of these questions, and insert them into the db
  197. $questionQuery = "SELECT `quiz_questions`.`quiz_sectionid` AS `quiz_sectionid`, " .
  198. "`quiz_questions`.`quiz_questionid` AS `quiz_questionid`, " .
  199. "`quiz_questions`.`quiz_questiontype` AS `quiz_questiontype`, " .
  200. "`quiz_questions`.`quiz_answermaxlength` AS `quiz_answermaxlength` " .
  201. "FROM `quiz_answersubmissions`, `quiz_questions` WHERE " .
  202. "`quiz_questions`.`page_modulecomponentid` = `quiz_answersubmissions`.`page_modulecomponentid` AND " .
  203. "`quiz_questions`.`quiz_sectionid` = `quiz_answersubmissions`.`quiz_sectionid` AND " .
  204. "`quiz_questions`.`quiz_questionid` = `quiz_answersubmissions`.`quiz_questionid` AND " .
  205. "`quiz_questions`.`page_modulecomponentid` = '{$this->quizId}' AND `user_id` = '$userId' AND " .
  206. "`quiz_questionviewtime` IS NOT NULL AND `quiz_answersubmittime` IS NULL ";
  207. if($this->quizRow['quiz_allowsectionrandomaccess'] == 1)
  208. $questionQuery .= "AND `quiz_answersubmissions`.`quiz_sectionid` = '".escape($_GET['sectionid']) ."'";
  209. $questionQuery .= "ORDER BY `quiz_answersubmissions`.`quiz_questionrank` LIMIT {$this->quizRow['quiz_questionsperpage']}";
  210. $questionResult = mysql_query($questionQuery);
  211. if (!$questionResult) {
  212. displayerror('Invalid query. ' . $questionQuery . ' ' . mysql_error());
  213. return false;
  214. }
  215. // Put in check about user's time elapsed here
  216. if ($this->checkUserTimedOut($userId, -1, '1 MINUTE')) {
  217. displayerror('Sorry, you have exceeded your time limit for the quiz. Your latest submission cannot be evaluated.');
  218. return false;
  219. }
  220. if ($this->quizRow['quiz_allowsectionrandomaccess']) {
  221. $sectionId = intval($_GET['sectionid']);
  222. if ($this->checkUserTimedOut($userId, $sectionId, '1 MINUTE')) {
  223. displayerror('Sorry, you have exceeded your time limit for this section. Your latest submission cannot be evaluated.');
  224. return false;
  225. }
  226. }
  227. $submittedAnswers = array();
  228. $rollbackQuery = array();
  229. while ($questionRow = mysql_fetch_assoc($questionResult)) {
  230. $rollbackQuery[] = "(`quiz_sectionid` = {$questionRow['quiz_sectionid']} AND `quiz_questionid` = {$questionRow['quiz_questionid']})";
  231. $questionType = $questionRow['quiz_questiontype'];
  232. if (!isset($_POST['hdnQuestion' . $questionRow['quiz_sectionid'] . '_' . $questionRow['quiz_questionid']])) {
  233. displayerror(
  234. 'Error. The answers that you submitted do not match the list of questions you were shown. You may have refreshed the page, and resubmitted your previous page\'s answers. ' .
  235. 'Please do not use the navigation buttons on your browser while taking the quiz.'
  236. );
  237. return false;
  238. }
  239. if ($questionType == 'sso' || $questionType == 'mso') {
  240. $options = getQuestionOptionList($this->quizId, $questionRow['quiz_sectionid'], $questionRow['quiz_questionid']);
  241. if ($questionType == 'sso') {
  242. $fieldName = 'optAnswer' . $questionRow['quiz_sectionid'] . '_' . $questionRow['quiz_questionid'];
  243. $submittedAnswer = isset($_POST[$fieldName]) && is_numeric($_POST[$fieldName]) ? intval($_POST[$fieldName]) : '';
  244. $optionFound = false;
  245. for ($i = 0; $i < count($options); ++$i) {
  246. if ($options[$i]['quiz_optionid'] == $submittedAnswer) {
  247. $submittedAnswers[] = array($questionRow['quiz_sectionid'], $questionRow['quiz_questionid'], $questionRow['quiz_questiontype'], $submittedAnswer);
  248. $optionFound = true;
  249. break;
  250. }
  251. }
  252. if (!$optionFound)
  253. $submittedAnswers[] = array($questionRow['quiz_sectionid'], $questionRow['quiz_questionid'], $questionRow['quiz_questiontype'], '');
  254. }
  255. else {
  256. $submittedAnswer = array();
  257. for ($i = 0; $i < count($options); ++$i) {
  258. $fieldName = 'chkAnswer' . $questionRow['quiz_sectionid'] . '_' . $questionRow['quiz_questionid'] . '_' . $options[$i]['quiz_optionid'];
  259. if (isset($_POST[$fieldName]) && is_numeric($_POST[$fieldName]))
  260. $submittedAnswer[] = intval($options[$i]['quiz_optionid']);
  261. }
  262. sort($submittedAnswer);
  263. $submittedAnswers[] = array($questionRow['quiz_sectionid'], $questionRow['quiz_questionid'], $questionRow['quiz_questiontype'], implode('|', $submittedAnswer));
  264. }
  265. }
  266. elseif ($questionType == 'subjective') {
  267. $fieldName = 'txtAnswer' . $questionRow['quiz_sectionid'] . '_' . $questionRow['quiz_questionid'];
  268. $submittedAnswers[] = array($questionRow['quiz_sectionid'], $questionRow['quiz_questionid'], $questionRow['quiz_questiontype'], isset($_POST[$fieldName]) ? escape($_POST[$fieldName]) : '');
  269. }
  270. }
  271. $rollbackQuery = "UPDATE `quiz_answersubmissions` SET `quiz_answersubmittime` = NULL WHERE `page_modulecomponentid` = {$this->quizId} AND `user_id` = $userId AND (" . implode(' OR ', $rollbackQuery) . ")";
  272. for ($i = 0; $i < count($submittedAnswers); ++$i) {
  273. $updateQuery = "UPDATE `quiz_answersubmissions` SET `quiz_submittedanswer` = '{$submittedAnswers[$i][3]}', `quiz_answersubmittime` = NOW() WHERE " .
  274. "`page_modulecomponentid` = {$this->quizId} AND `quiz_sectionid` = '{$submittedAnswers[$i][0]}' AND " .
  275. "`quiz_questionid` = '{$submittedAnswers[$i][1]}' AND `user_id` = '$userId'";
  276. if (!mysql_query($updateQuery)) {
  277. displayerror('Invalid Query. Could not save answers.');
  278. mysql_query($rollbackQuery);
  279. return false;
  280. }
  281. }
  282. return true;
  283. }
  284. /**
  285. * function checkQuizInitialized:
  286. * Checks if a quiz has been initialized.
  287. * @param Integer $userId User ID of the user.
  288. * @return Boolean True or false to indicate whether the quiz has been initialized.
  289. */
  290. private function checkQuizInitialized($userId) {
  291. $countQuery = "SELECT COUNT(*) FROM `quiz_answersubmissions` WHERE `page_modulecomponentid` = '{$this->quizId}' AND `user_id` = '$userId'";
  292. $countResult = mysql_query($countQuery);
  293. $countRow = mysql_fetch_row($countResult);
  294. return $countRow[0] == $this->quizRow['quiz_questionspertest'];
  295. }
  296. /**
  297. * function initQuiz:
  298. * Performs necessary operations before a user starts a quiz.
  299. * @param Integer $userId User ID.
  300. * @return Boolean True indicating success, false indicating errors.
  301. */
  302. public function initQuiz($userId) {
  303. // a user is about to start the quiz
  304. // generate a list of questions, insert into quiz_answersubmissions, with answersubmittime = NULL
  305. if ($this->checkQuizInitialized($userId))
  306. return true;
  307. $this->deleteEntries($userId);
  308. $sectionList = getSectionList($this->quizId);
  309. $questionList = array();
  310. $sections = array();
  311. for ($i = 0; $i < count($sectionList); ++$i) {
  312. $questionList[$i] = $this->getSectionQuestions($sectionList[$i]);
  313. for ($j = 0; $j < count($questionList[$i]); ++$j)
  314. $sections[] = $i;
  315. }
  316. if ($this->quizRow['quiz_allowsectionrandomaccess'] == 0 && $this->quizRow['quiz_mixsections'])
  317. shuffle($sections);
  318. $offsets = array_fill(0, count($questionList), 0);
  319. for ($i = 0; $i < count($sections); ++$i) {
  320. $insertQuery = "INSERT INTO `quiz_answersubmissions`(`page_modulecomponentid`, `quiz_sectionid`, `quiz_questionid`, `user_id`, `quiz_questionrank`) VALUES" .
  321. "({$this->quizId}, {$sectionList[$sections[$i]]['quiz_sectionid']}, {$questionList[$sections[$i]][$offsets[$sections[$i]]]}, $userId, $i)";
  322. if (!mysql_query($insertQuery)) {
  323. displayerror('Database Error. Could not initialize quiz.');
  324. return false;
  325. }
  326. $offsets[$sections[$i]]++;
  327. }
  328. return true;
  329. }
  330. /**
  331. * function deleteEntries:
  332. * Deletes all entries for a particular user.
  333. */
  334. public function deleteEntries($userId) {
  335. $tableNames = array('quiz_userattempts', 'quiz_answersubmissions');
  336. $affectedRows = array();
  337. return deleteItem($tableNames, "`page_modulecomponentid` = {$this->quizId} AND `user_id` = $userId", $affectedRows);
  338. }
  339. /**
  340. * function getPageQuestions:
  341. * returns questions to be displayed in this page, ie questions for which answer has not been submitted yet
  342. */
  343. private function getPageQuestions($userId, $sectionId = -1) {
  344. $questionsPerPage = $this->quizRow['quiz_questionsperpage'];
  345. $questionQuery = "SELECT `quiz_sectionid`, `quiz_questionid` FROM `quiz_answersubmissions` WHERE `user_id` = '$userId' AND `page_modulecomponentid` = '{$this->quizId}' AND `quiz_answersubmittime` IS NULL ";
  346. if ($this->quizRow['quiz_allowsectionrandomaccess'] == 1)
  347. $questionQuery .= " AND `quiz_sectionid` = '$sectionId' ";
  348. $questionQuery .= " ORDER BY `quiz_questionrank` LIMIT $questionsPerPage";
  349. $questionResult = mysql_query($questionQuery);
  350. if (!$questionResult) {
  351. displayerror('Database Error. Could not fetch questions.');
  352. return null;
  353. }
  354. $questionIds = array();
  355. while ($questionRow = mysql_fetch_row($questionResult))
  356. $questionIds[] = $questionRow;
  357. return $questionIds;
  358. }
  359. /**
  360. * function getTimerHtml:
  361. * returns HTML timer code and invokes JSTimer to take care of running timer in browser
  362. * @see ./timer.js
  363. */
  364. private function getTimerHtml($userId, $sectionId = -1) {
  365. $testElapsedTime = $this->getElapsedTime($userId);
  366. $testElapsedTime = explode(':', $testElapsedTime);
  367. $testElapsedTime = implode(', ', $testElapsedTime);
  368. $sectionElapsedTime = $this->getElapsedTime($userId,$sectionId);
  369. $sectionElapsedTime = explode(':', $sectionElapsedTime);
  370. $sectionElapsedTime = implode(', ', $sectionElapsedTime);
  371. $testTime = $this->quizRow['quiz_testduration'];
  372. $testTime = explode(':', $testTime);
  373. $testTime = implode(', ', $testTime);
  374. if ($this->quizRow['quiz_allowsectionrandomaccess']) {
  375. $sectionTime = mysql_fetch_array(mysql_query("SELECT `quiz_sectiontimelimit` FROM `quiz_sections` WHERE `page_modulecomponentid` = '{$this->quizId}' AND `quiz_sectionid` = '$sectionId'"));
  376. $sectionTime = $sectionTime[0];
  377. $sectionTime = explode(':', $sectionTime);
  378. $sectionTime = implode(', ', $sectionTime);
  379. $scripts[] = "var sectionTimer = new JSTimer('sectionTimerContainer', $sectionElapsedTime);\nsectionTimer.addTickHandler($sectionTime, forceQuizSubmit)";
  380. }
  381. $scripts[] = "var testTimer = new JSTimer('testTimerContainer', $testElapsedTime);\ntestTimer.addTickHandler($testTime, forceQuizSubmit)";
  382. $divs = array();
  383. if ($this->quizRow['quiz_showquiztimer']) {
  384. $divs[] = '<div id="testTimerContainer" class="quiz_testtimer">Total Quiz Time Elapsed: </div>';
  385. }
  386. if ($this->quizRow['quiz_showpagetimer']) {
  387. $divs[] = '<div id="pageTimerContainer" class="quiz_pagetimer"></div>';
  388. $scripts[] = "var pageTimer = new JSTimer('pageTimerContainer', 0, 0, 0);\n";
  389. }
  390. $sectionRow = getSectionRow($this->quizId, $sectionId);
  391. if ($sectionRow['quiz_sectionshowlimit']) {
  392. $sectionRow = getSectionRow($this->quizId,$sectionId);
  393. $limit = $sectionRow['quiz_sectiontimelimit'];
  394. $divs[] = '<div id="pageTimerlimit" class="quiz_limit">Section Limit: ' . $limit . '</div>';
  395. $divs[] = '<div id="sectionTimerContainer"
  396. class="quiz_testtimer">Section Time Elapsed: </div><br /><br />';
  397. }
  398. global $urlRequestRoot, $cmsFolder, $moduleFolder;
  399. $timerScriptSrc = "$urlRequestRoot/$cmsFolder/$moduleFolder/quiz/timer.js";
  400. if (count($divs)) {
  401. $divs = implode("\n", $divs);
  402. $scripts = implode("\n", $scripts);
  403. $timerScript = <<<TIMERSCRIPT
  404. <script type="text/javascript" src="$timerScriptSrc"></script>
  405. $divs
  406. <script type="text/javascript">
  407. function forceQuizSubmit() {
  408. alert("Your time is up. Please click Ok to submit the quiz. If you do not submit within 30 seconds, your quiz will expire, and your answers to this page will not be recorded.");
  409. var quizForm = document.getElementById('quizForm');
  410. var submitButton = document.getElementById('btnSubmit');
  411. submitButton.type = 'hidden';
  412. quizForm.submit();
  413. }
  414. $scripts
  415. </script>
  416. TIMERSCRIPT;
  417. }
  418. return $timerScript;
  419. }
  420. /**
  421. * function formatQuestion:
  422. * Given a question row, return HTML for the question.
  423. * @param $questionRow
  424. * @return string Question in HTML.
  425. */
  426. private function formatQuestion($questionRow, $questionNumber = -1) {
  427. $questionType = $questionRow['quiz_questiontype'];
  428. if ($questionType == 'subjective') {
  429. $fieldName = 'txtAnswer' . $questionRow['quiz_sectionid'] . '_' . $questionRow['quiz_questionid'];
  430. $answer = '<textarea
  431. style="width:95%;height:100px;" name="' .
  432. $fieldName . '" id="' . $fieldName . '"></textarea>';
  433. }
  434. else {
  435. $optionList = getQuestionOptionList($this->quizId, $questionRow['quiz_sectionid'], $questionRow['quiz_questionid']);
  436. $answer = '<table class="objectivecontainer" width="100%">';
  437. for ($i = 0; $i < count($optionList); ++$i) {
  438. $fieldType = ($questionType == 'sso' ? 'radio' : 'checkbox');
  439. $fieldName = '';
  440. $fieldId = '';
  441. if ($questionType == 'sso') {
  442. $fieldName = 'optAnswer' . $questionRow['quiz_sectionid'] . '_' . $questionRow['quiz_questionid'];
  443. $fieldId = $fieldName . '_' . $optionList[$i]['quiz_optionid'];
  444. }
  445. elseif ($questionType == 'mso') {
  446. $fieldName = 'chkAnswer' . $questionRow['quiz_sectionid'] . '_' . $questionRow['quiz_questionid'] . '_' . $optionList[$i]['quiz_optionid'];
  447. $fieldId = $fieldName;
  448. }
  449. $answer .= "<tr><td width=\"24\"><input type=\"$fieldType\" name=\"$fieldName\" id=\"$fieldId\" value=\"{$optionList[$i]['quiz_optionid']}\" /> </td><td><label for=\"$fieldId\"> {$optionList[$i]['quiz_optiontext']}</label></td></tr>\n";
  450. }
  451. $answer .= '</table>';
  452. }
  453. $hiddenFieldName = "hdnQuestion{$questionRow['quiz_sectionid']}_{$questionRow['quiz_questionid']}";
  454. $questionDesc = $questionRow['quiz_question'];
  455. if ($questionNumber > 0) $questionDesc = $questionNumber . ') ' . $questionDesc;
  456. global $sourceFolder, $moduleFolder;
  457. require_once($sourceFolder."/pngRender.class.php");
  458. $render = new pngrender();
  459. $questionDesc = $render->transform($questionDesc);
  460. $answer = $render->transform($answer);
  461. return <<<QUESTIONFORM
  462. <input type="hidden" name="$hiddenFieldName" id="$hiddenFieldName" value="" />
  463. <div class="quiz_questioncontainer">
  464. <br /><b>{$questionDesc}</b><br /><br />
  465. </div>
  466. <div class="quiz_answercontainer">
  467. $answer
  468. </div>
  469. QUESTIONFORM;
  470. }
  471. /**
  472. * function formatNextPage:
  473. * Returns an HTML page containing the next set of questions for the user.
  474. */
  475. private function formatNextPage($userId, $sectionId = -1) {
  476. $questionCount = $this->quizRow['quiz_questionsperpage'];
  477. $questionQuery = "SELECT `quiz_questions`.`quiz_sectionid` AS `quiz_sectionid`, `quiz_questions`.`quiz_questionid` AS `quiz_questionid`, `quiz_question`, `quiz_questiontype`, `quiz_questionweight`, `quiz_answermaxlength`, `quiz_rightanswer`, `quiz_questionviewtime`, `quiz_answersubmittime` " .
  478. "FROM `quiz_questions`, `quiz_answersubmissions` WHERE " .
  479. "`quiz_questions`.`page_modulecomponentid` = '{$this->quizId}' AND " .
  480. "`quiz_answersubmissions`.`user_id` = '$userId' AND " .
  481. "`quiz_questions`.`page_modulecomponentid` = `quiz_answersubmissions`.`page_modulecomponentid` AND " .
  482. "`quiz_questions`.`quiz_sectionid` = `quiz_answersubmissions`.`quiz_sectionid` AND " .
  483. "`quiz_questions`.`quiz_questionid` = `quiz_answersubmissions`.`quiz_questionid` AND " .
  484. "`quiz_answersubmissions`.`quiz_answersubmittime` IS NULL ";
  485. if ($this->quizRow['quiz_allowsectionrandomaccess'] == 1)
  486. $questionQuery .= "AND `quiz_answersubmissions`.`quiz_sectionid` = '$sectionId' ";
  487. $questionQuery .= "ORDER BY `quiz_answersubmissions`.`quiz_questionrank` " .
  488. "LIMIT $questionCount";
  489. $questionResult = mysql_query($questionQuery);
  490. $questionNumber = 1;
  491. $questionPage = $this->getTimerHtml($userId, $sectionId);
  492. $questionPage .= '<form name="quizquestions" id="quizForm" method="POST" action="./+view' . ($sectionId == -1 ? '' : '&sectionid=' . $sectionId) . '" onsubmit="return confirm(\'Are you sure you wish to submit this page?\')">';
  493. while ($questionRow = mysql_fetch_assoc($questionResult)) {
  494. if (is_null($questionRow['quiz_questionviewtime']))
  495. mysql_query("UPDATE `quiz_answersubmissions` SET `quiz_questionviewtime` = NOW() WHERE `page_modulecomponentid` = '{$this->quizId}' AND `quiz_sectionid` = '{$questionRow['quiz_sectionid']}' AND `quiz_questionid` = '{$questionRow['quiz_questionid']}'");
  496. $questionPage .= $this->formatQuestion($questionRow, $questionNumber);
  497. ++$questionNumber;
  498. }
  499. $questionPage .= '<input type="submit" name="btnSubmit" id="btnSubmit" value="Submit" />';
  500. $questionPage .= '</form>';
  501. $questionPage .= <<<QUESTIONPAGESCRIPT
  502. <script type="text/javascript">
  503. // make opt buttons uncheckable
  504. var inputFields = document.getElementById('quizForm').getElementsByTagName('input');
  505. for (var i = 0; i < inputFields.length; ++i) {
  506. if (inputFields[i].type == 'radio')
  507. inputFields[i].onclick = function(e) {
  508. if (this.rel == 'checked') {
  509. this.checked = false;
  510. this.rel = '';
  511. }
  512. else {
  513. var elements = document.getElementsByName(this.name);
  514. for (var i = 0; i < elements.length; ++i) {
  515. elements[i].rel = '';
  516. elements[i].checked = false;
  517. }
  518. this.checked = true;
  519. this.rel = 'checked';
  520. }
  521. };
  522. }
  523. </script>
  524. QUESTIONPAGESCRIPT;
  525. return $questionPage;
  526. }
  527. /**
  528. * function countAttemptedQuestions:
  529. * Counts the number of questions a user has submitted in a given section.
  530. * @param Integer $userId User ID of the user.
  531. * @param Integer $sectionId Section ID of the user. If omitted, total number of questions attempted in the quiz are counted.
  532. * @return Integer Number of questions attempted. False in case of errors.
  533. */
  534. private function countAttemptedQuestions($userId, $sectionId = -1) {
  535. $countQuery = "SELECT COUNT(*) FROM `quiz_submittedanswers` WHERE `page_modulecomponentid` = '{$this->quizId}'";
  536. if ($sectionId != -1)
  537. $countQuery .= " AND `quiz_sectionid` = '$sectionId'";
  538. $countQuery .= " `user_id` = $userId AND `quiz_answersubmittime` IS NOT NULL";
  539. $countResult = mysql_query($countQuery);
  540. if (!$countResult) {
  541. displayerror('Database Error. Could not retrieve user attempt information.');
  542. return false;
  543. }
  544. $countRow = mysql_fetch_row($countResult);
  545. return $countRow[0];
  546. }
  547. /**
  548. * function getSectionQuestions:
  549. * gets list of questionId in this section considering, whether quiz is randomized and number of questions per section
  550. */
  551. private function getSectionQuestions($sectionRow) {
  552. $questionTypes = array_keys(getQuestionTypes());
  553. $sectionId = $sectionRow['quiz_sectionid'];
  554. if ($sectionRow['quiz_sectionquestionshuffled'] == 0) {
  555. $limit = 0;
  556. for ($i = 0; $i < count($questionTypes); ++$i)
  557. $limit += $sectionRow["quiz_section{$questionTypes[$i]}count"];
  558. $questionQuery = "SELECT `quiz_questionid` FROM `quiz_questions` WHERE `page_modulecomponentid` = '{$this->quizId}' AND `quiz_sectionid` = '$sectionId' ORDER BY `quiz_questionrank` LIMIT $limit";
  559. }
  560. else {
  561. $questionIdQueries = array();
  562. for ($i = 0; $i < count($questionTypes); ++$i) {
  563. $limit = $sectionRow["quiz_section{$questionTypes[$i]}count"];
  564. if ($limit) {
  565. $questionIdQueries[] =
  566. "(SELECT `quiz_questionid` FROM `quiz_questions` WHERE `page_modulecomponentid` = '{$this->quizId}' AND `quiz_sectionid` = '$sectionId' AND `quiz_questiontype` = '{$questionTypes[$i]}' ORDER BY RAND() LIMIT $limit)";
  567. }
  568. }
  569. $questionQuery = "SELECT `quiz_questionid` FROM (" . implode(' UNION ', $questionIdQueries) . ") AS `questions` ORDER BY RAND()";
  570. }
  571. $questionIds = array();
  572. $questionResult = mysql_query($questionQuery) or die(mysql_error());
  573. while ($questionRow = mysql_fetch_row($questionResult))
  574. $questionIds[] = $questionRow[0];
  575. return $questionIds;
  576. }
  577. /**
  578. * function checkQuizCompleted:
  579. * Checks whether a user has completed a quiz, by checking whether the user has completed
  580. * all sections under that quiz.
  581. * @param Integer $userId User ID.
  582. * @return Boolean True or false indicating whether the quiz has been completed.
  583. */
  584. private function checkQuizCompleted($userId) {
  585. $countQuery = "SELECT COUNT(*) FROM `quiz_userattempts`, `quiz_sections` WHERE " .
  586. "`quiz_sections`.`page_modulecomponentid` = `quiz_userattempts`.`page_modulecomponentid` AND " .
  587. "`quiz_sections`.`quiz_sectionid` = `quiz_userattempts`.`quiz_sectionid` AND " .
  588. "`quiz_sections`.`page_modulecomponentid` = '{$this->quizId}' AND " .
  589. "`quiz_userattempts`.`user_id` = '$userId' AND " .
  590. "`quiz_submissiontime` IS NOT NULL";
  591. $countResult = mysql_query($countQuery);
  592. if (!$countResult) {
  593. displayerror('Database Error. Could not fetch section information.');
  594. return false;
  595. }
  596. $countRow = mysql_fetch_row($countResult);
  597. $completedCount = $countRow[0];
  598. $countQuery = "SELECT COUNT(*) FROM `quiz_sections` WHERE `page_modulecomponentid` = '{$this->quizId}'";
  599. $countResult = mysql_query($countQuery);
  600. $countRow = mysql_fetch_row($countResult);
  601. return $countRow[0] == $completedCount;
  602. }
  603. /**
  604. * function isValidId:
  605. * Checks whether an ID is valid.
  606. * @param Integer $id The ID to test.
  607. * @return Boolean Whether the ID is valid or not.
  608. */
  609. private function isValidId($id) {
  610. return isset($id) && is_numeric($id) && $id > 0;
  611. }
  612. /**
  613. * function markSectionCompleted:
  614. * Marks a section as completed.
  615. * @return Boolean True if the section is (or was) completed. False if the section is not complete.
  616. */
  617. private function markSectionCompleted($userId, $sectionId = -1) {
  618. if ($sectionId == -1) {
  619. $sections = getSectionList($this->quizId);
  620. $allOk = true;
  621. for ($i = 0; $i < count($sections); ++$i)
  622. $allOk = $this->markSectionCompleted($userId, $sections[$i]['quiz_sectionid']) && $allOk;
  623. return $allOk;
  624. }
  625. $attemptRow = getAttemptRow($this->quizId, $sectionId, $userId);
  626. if (is_null($attemptRow['quiz_submissiontime'])) {
  627. // Check if all questions for this section have been completed, if yes, set quiz_submissiontime and return true
  628. $questionQuery = "SELECT COUNT(*) FROM `quiz_answersubmissions` WHERE " .
  629. "`page_modulecomponentid` = '{$this->quizId}' AND `quiz_sectionid` = '$sectionId' AND `user_id` = '$userId' AND `quiz_answersubmittime` IS NULL";
  630. $questionResult = mysql_query($questionQuery);
  631. $questionRow = mysql_fetch_row($questionResult);
  632. if ($questionRow[0] != 0)
  633. return false;
  634. $updateQuery = "UPDATE `quiz_userattempts` SET `quiz_submissiontime` = NOW() WHERE `page_modulecomponentid` = '$this->quizId' AND `quiz_sectionid` = '$sectionId' AND `user_id` = '$userId'";
  635. if (mysql_query($updateQuery))
  636. return true;
  637. else {
  638. displayerror('Database Error. Could not mark section as completed.');
  639. return -1;
  640. }
  641. }
  642. else
  643. return true;
  644. }
  645. /**
  646. * function markQuizCompleted:
  647. * Mark a Quiz as completed. Fill all unsubmitted answers with '', and set quiz as completed.
  648. * To be used only when a user's quiz times out.
  649. * @param Integer $userId User ID.
  650. */
  651. private function markQuizCompleted($userId) {
  652. $updateQueries = array(
  653. "UPDATE `quiz_answersubmissions` SET `quiz_submittedanswer` = '', `quiz_answersubmittime` = NOW() WHERE `page_modulecomponentid` = {$this->quizId} AND `user_id` = '$userId' AND `quiz_answersubmittime` IS NULL",
  654. "UPDATE `quiz_userattempts` SET `quiz_submissiontime` = NOW() WHERE `page_modulecomponentid` = '{$this->quizId}' AND `user_id` = '$userId' AND `quiz_submissiontime` IS NULL"
  655. );
  656. if (!mysql_query($updateQueries[0]) || !mysql_query($updateQueries[1])) {
  657. displayerror('Error. Could not mark quiz as completed.');
  658. return false;
  659. }
  660. return true;
  661. }
  662. /**
  663. * function getElapsedTime:
  664. * Returns the time a user has spent on a section or a quiz.
  665. * @param Integer $userId User ID.
  666. * @param Integer $sectionId Section ID. Optional.
  667. */
  668. private function getElapsedTime($userId, $sectionId = -1) {
  669. if ($sectionId < 0)
  670. $elapsedQuery = "SELECT TIMEDIFF(NOW(), MIN(`quiz_attemptstarttime`)) FROM `quiz_userattempts` WHERE " .
  671. "`page_modulecomponentid` = '{$this->quizId}' AND `user_id` = '$userId'";
  672. else
  673. $elapsedQuery = "SELECT TIMEDIFF(NOW(), `quiz_attemptstarttime`) FROM `quiz_userattempts` WHERE " .
  674. "`page_modulecomponentid` = '{$this->quizId}' AND `quiz_sectionid` = '$sectionId' AND `user_id` = '$userId'";
  675. $elapsedResult = mysql_query($elapsedQuery);
  676. if (!$elapsedResult)
  677. displayerror('Error. ' . $elapsedQuery . '<br />' . mysql_error());
  678. $elapsedRow = mysql_fetch_row($elapsedResult);
  679. return $elapsedRow[0];
  680. }
  681. private function getRemainingTime($userId, $sectionId = -1) {
  682. if ($sectionId < 0) {
  683. $remainingQuery = "SELECT TIMEDIFF(NOW(), ADDTIME(MIN(`quiz_attemptstarttime`), '{$this->quizRow['quiz_testduration']}')) FROM `quiz_userattempts` WHERE " .
  684. "`page_modulecomponentid` = '{$this->quizId}' AND `user_id` = '$userId'";
  685. }
  686. else {
  687. $remainingQuery = "SELECT TIMEDIFF(NOW(), ADDTIME(`quiz_attemptstarttime`, '{$this->quizRow['quiz_testduration']}')) FROM `quiz_userattempts` WHERE " .
  688. "`page_modulecomponentid` = '{$this->quizId}' AND `user_id` = '$userId'";
  689. }
  690. $remainingResult = mysql_query($remainingQuery);
  691. $remainingRow = mysql_fetch_row($remainingResult);
  692. return $remainingRow[0];
  693. }
  694. /**
  695. * function checkUserTimedOut:
  696. * Returns a string denoting the amount of time the user has to complete the section or quiz.
  697. * @param Integer $userId User ID of the user.
  698. * @param Integer $sectionId Section ID. If omitted, the time remaining for the entire quiz is shown.
  699. * @param String $offset Amount of time to add as grace period for the user. To be used only to check if a page can be submitted.
  700. * @return Boolean indicating whether the user has run out of time or not. -1 indicating errors.
  701. */
  702. private function checkUserTimedOut($userId, $sectionId = -1, $offset = '0 SECOND') {
  703. if ($sectionId < 0) {
  704. // Check if the quiz has timed out:
  705. // Find the earliest attempt start time, add quiz duration to it
  706. // add offset to now, and compare
  707. $timeoutQuery = "SELECT IF(DATE_SUB(NOW(), INTERVAL $offset) > ADDTIME(MIN(`quiz_attemptstarttime`), '{$this->quizRow['quiz_testduration']}'), 1, 0) AS `quiz_expired` FROM " .
  708. "`quiz_userattempts` WHERE `page_modulecomponentid` = {$this->quizId} AND `user_id` = $userId";
  709. }
  710. else {
  711. $sectionRow = getSectionRow($this->quizId, $sectionId);
  712. if ($sectionRow['quiz_sectiontimelimit'] == '00:00:00')
  713. return false;
  714. $timeoutQuery = "SELECT IF(DATE_SUB(NOW(), INTERVAL $offset) > ADDTIME(`quiz_attemptstarttime`, '{$sectionRow['quiz_sectiontimelimit']}'), 1, 0) AS `quiz_expired` FROM " .
  715. "`quiz_userattempts` WHERE `page_modulecomponentid` = '{$this->quizId}' AND `quiz_sectionid` = '$sectionId' AND `user_id` = '$userId'";
  716. }
  717. $timeoutResult = mysql_query($timeoutQuery);
  718. if (!$timeoutResult) {
  719. displayerror('Database Error. Could not retrieve time information.');
  720. return -1;
  721. }
  722. $timeoutRow = mysql_fetch_row($timeoutResult);
  723. if (is_null($timeoutRow[0])) {
  724. // An invalid Section ID was passed => we could not find a row for the user for that
  725. // Section ID. assume he timed out
  726. return true;
  727. }
  728. return $timeoutRow[0];
  729. }
  730. /**
  731. * function forceQuizCompleted:
  732. * Forcefully marks a quiz or a section as completed.
  733. * @param Integer $userId User ID.
  734. * @param Integer $sectionId Section ID.
  735. * @return Boolean True indicating success, false indicating failure.
  736. */
  737. private function forceQuizCompleted($userId, $sectionId = -1) {
  738. $updateQuery = "UPDATE `quiz_userattempts` SET `quiz_submissiontime` = NOW() WHERE `quiz_submissiontime` IS NULL AND `page_modulecomponentid` = '{$this->quizId}' AND `user_id` = '$userId'";
  739. if ($sectionId >= 0)
  740. $updateQuery .= " AND `quiz_sectionid` = '$sectionId'";
  741. if (!mysql_query($updateQuery)) {
  742. displayerror('Database Error. Could not mark quiz as completed.');
  743. return false;
  744. }
  745. return true;
  746. }
  747. };