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