/cms/modules/quiz/simplequiz.php
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§ionid=' . $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 ? '' : '§ionid=' . $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};