tagging as ATutor 1.5.4-release
[atutor.git] / include / classes / testQuestions.class.php
1 <?php
2 /****************************************************************/
3 /* ATutor                                                                                                               */
4 /****************************************************************/
5 /* Copyright (c) 2002-2007 by Greg Gay & Joel Kronenberg        */
6 /* Adaptive Technology Resource Centre / University of Toronto  */
7 /* http://atutor.ca                                                                                             */
8 /*                                                              */
9 /* This program is free software. You can redistribute it and/or*/
10 /* modify it under the terms of the GNU General Public License  */
11 /* as published by the Free Software Foundation.                                */
12 /****************************************************************/
13 // $Id$
14
15 /*
16  * Steps to follow when adding a new question type:
17  *
18  * 1 - Create a class extending AbstractQuestion or extend an 
19  *     existing question class.
20  *     Define $sPrefix and $sNameVar appropriately.
21  *     Implement the following methods, which set template variables:
22  *
23  *        assignQTIVariables()
24  *        assignDisplayResultVariables()
25  *        assignDisplayVariables()
26  *        assignDisplayStatisticsVariables()
27  *     
28  *     And implement mark() which is used for marking the result.
29  *
30  * 2 - Add the new class name to $question_classes in test_question_factory()
31  *
32  * 3 - Add $sNameVar to the language database.
33  *
34  * 4 - Create the following files for creating and editing the question,
35  *     where "{PREFIX}" is the value defined by $sPrefix:
36  *
37  *     /tools/tests/create_question_{PREFIX}.php
38  *     /tools/tests/edit_question_{PREFIX}.php
39  *
40  * 5 - Add those two newly created pages to 
41  *     /mods/_standard/tests/module.php
42  *
43  * 6 - Create the following template files:
44  *
45  *     /themes/default/test_questions/{PREFIX}.tmpl.php
46  *     /themes/default/test_questions/{PREFIX}_qti_2p1.tmpl.php
47  *     /themes/default/test_questions/{PREFIX}_result.tmpl.php
48  *     /themes/default/test_questions/{PREFIX}_stats.tmpl.php
49  *
50  * 7 - Done!
51  **/
52
53 class TestQuestions {
54         // returns array of prefix => name, sorted!
55         /*static */function getQuestionPrefixNames() {
56                 $question_prefix_names = array(); // prefix => name
57                 $questions = TestQuestions::getQuestionClasses();
58                 foreach ($questions as $type => $question) {
59                         $o = TestQuestions::getQuestion($type);
60                         $question_prefix_names[$o->getPrefix()] = $o->getName();
61                 }
62                 asort($question_prefix_names);
63                 return $question_prefix_names;
64         }
65
66         /*static */function getQuestionClasses() {
67                 /** NOTE: The indices are CONSTANTS. Do NOT change!! **/
68                 $question_classes = array(); // type ID => class name
69                 $question_classes[1] = 'MultichoiceQuestion';
70                 $question_classes[2] = 'TruefalseQuestion';
71                 $question_classes[3] = 'LongQuestion';
72                 $question_classes[4] = 'LikertQuestion';
73                 $question_classes[5] = 'MatchingQuestion';
74                 $question_classes[6] = 'OrderingQuestion';
75                 $question_classes[7] = 'MultianswerQuestion';
76                 $question_classes[8] = 'MatchingddQuestion';
77
78                 return $question_classes;
79         }
80
81         /**
82          * Used to create question objects based on $question_type.
83          * A singleton that creates one obj per question since
84          * questions are all stateless.
85          * Returns a reference to the question object.
86          */
87         /*static */function & getQuestion($question_type) {
88                 static $objs, $question_classes;
89
90                 if (isset($objs[$question_type])) {
91                         return $objs[$question_type];
92                 }
93
94                 $question_classes = TestQuestions::getQuestionClasses();
95
96                 if (isset($question_classes[$question_type])) {
97                         global $savant;
98                         $objs[$question_type] =& new $question_classes[$question_type]($savant);
99                 } else {
100                         return FALSE;
101                 }
102
103                 return $objs[$question_type];
104         }
105 }
106
107 function test_question_qti_export(/* array */ $question_ids) {
108         require(AT_INCLUDE_PATH.'classes/zipfile.class.php'); // for zipfile
109         require(AT_INCLUDE_PATH.'lib/html_resource_parser.inc.php'); // for get_html_resources()
110         require(AT_INCLUDE_PATH.'classes/XML/XML_HTMLSax/XML_HTMLSax.php');     // for XML_HTMLSax
111
112         global $savant, $db, $system_courses, $languageManager;
113
114         $course_language = $system_courses[$_SESSION['course_id']]['primary_language'];
115         $courseLanguage =& $languageManager->getLanguage($course_language);
116         $course_language_charset = $courseLanguage->getCharacterSet();
117
118         $zipfile = new zipfile();
119         $zipfile->create_dir('resources/'); // for all the dependency files
120         $resources    = array();
121         $dependencies = array();
122
123         asort($question_ids);
124
125         $question_ids_delim = implode(',',$question_ids);
126         $sql = "SELECT * FROM ".TABLE_PREFIX."tests_questions WHERE course_id=$_SESSION[course_id] AND question_id IN($question_ids_delim)";
127         $result = mysql_query($sql, $db);
128
129         while ($row = mysql_fetch_assoc($result)) {
130                 $obj = test_question_factory($row['type']);
131                 $xml = $obj->exportQTI($row, $course_language_charset);
132                 $local_dependencies = array();
133
134                 $text_blob = implode(' ', $row);
135                 $local_dependencies = get_html_resources($text_blob);
136                 $dependencies = array_merge($dependencies, $local_dependencies);
137
138                 $resources[] = array('href'         => 'question_'.$row['question_id'].'.xml',
139                                                          'dependencies' => array_keys($local_dependencies));
140
141                 $zipfile->add_file($xml, 'question_'.$row['question_id'].'.xml');
142         }
143
144         // add any dependency files:
145         foreach ($dependencies as $resource => $resource_server_path) {
146                 $zipfile->add_file(@file_get_contents($resource_server_path), 'resources/' . $resource, filemtime($resource_server_path));
147         }
148
149         // construct the manifest xml
150         $savant->assign('resources', $resources);
151         $savant->assign('dependencies', array_keys($dependencies));
152         $savant->assign('encoding', $course_language_charset);
153         $manifest_xml = $savant->fetch('test_questions/manifest_qti_2p1.tmpl.php');
154
155         $zipfile->add_file($manifest_xml, 'imsmanifest.xml');
156
157         $zipfile->close();
158
159         $filename = str_replace(array(' ', ':'), '_', $_SESSION['course_title'].'-'._AT('question_database').'-'.date('Ymd'));
160         $zipfile->send_file($filename);
161         exit;
162 }
163
164 /**
165 * keeps count of the question number (when displaying the question)
166 * need this function because PHP 4 doesn't support static members
167 */
168 function TestQuestionCounter($increment = FALSE) {
169         static $count;
170
171         if (!isset($count)) { 
172                 $count = 0;
173         }
174         if ($increment) {
175                 $count++;
176         }
177
178         return $count;
179 }
180
181
182 /**
183  * testQuestion
184  *
185  * Note that all PHP 5 OO declarations and signatures are commented out to be
186  * backwards compatible with PHP 4.
187  *
188  */
189 /*abstract */ class AbstractTestQuestion  {
190         /**
191         * Savant2 $savant - refrence to the savant obj
192         */
193         /*protected */ var $savant;
194
195         /**
196         * Constructor method.  Initialises variables.
197         */
198         function AbstractTestQuestion(&$savant) { $this->savant =& $savant; }
199
200         /**
201         * Public
202         */
203         /*final public */function seed($salt) {
204                 /**
205                 * by controlling the seed before calling array_rand() we insure that
206                 * we can un-randomize the order for marking.
207                 * used with ordering type questions only.
208                 */
209                 srand($salt + ord(DB_PASSWORD) + $_SESSION['member_id']);
210         }
211
212         /**
213         * Public
214         * Prints the name of this question
215         */
216         /*final public */function printName() { echo $this->getName(); }
217
218         /**
219         * Public
220         * Prints the name of this question
221         */
222         /*final public */function getName() { return _AT($this->sNameVar); }
223
224         /**
225         * Public
226         * Returns the prefix string (used for file names)
227         */
228         /*final public */function getPrefix() { return $this->sPrefix; }
229
230         /**
231         * Display the current question (for taking or previewing a test/question)
232         */
233         /*final public */function display($row) {
234                 // print the generic question header
235                 $this->displayHeader($row['weight']);
236
237                 // print the question specific template
238                 $this->assignDisplayVariables($row);
239                 $this->savant->display('test_questions/' . $this->sPrefix . '.tmpl.php');
240                 
241                 // print the generic question footer
242                 $this->displayFooter();
243         }
244
245         /**
246         * Display the result for the current question
247         */
248         /*final public */function displayResult($row, $answer_row, $editable = FALSE) {
249                 // print the generic question header
250                 $this->displayHeader($row['weight'], (int) $answer_row['score'], $editable ? $row['question_id'] : FALSE);
251
252                 // print the question specific template
253                 $this->assignDisplayResultVariables($row, $answer_row);
254                 $this->savant->display('test_questions/' . $this->sPrefix . '_result.tmpl.php');
255                 
256                 // print the generic question footer
257                 $this->displayFooter();
258         }
259
260
261         /**
262         * print the question template header
263         */
264         /*final public */function displayResultStatistics($row, $answers) {
265                 TestQuestionCounter(TRUE);
266
267                 $this->assignDisplayStatisticsVariables($row, $answers);
268                 $this->savant->display('test_questions/' . $this->sPrefix . '_stats.tmpl.php');
269         }
270
271         /*final public */function exportQTI($row, $encoding) {
272                 $this->savant->assign('encoding', $encoding);
273                 $this->assignQTIVariables($row);
274                 $xml = $this->savant->fetch('test_questions/'. $this->sPrefix . '_qti_2p1.tmpl.php');
275
276                 return $xml;
277         }
278
279         /**
280         * print the question template header
281         */
282         /*final private */function displayHeader($weight, $score = FALSE, $question_id = FALSE) {
283                 TestQuestionCounter(TRUE);
284
285                 $this->savant->assign('question_id', $question_id);
286                 $this->savant->assign('score', $score);
287                 $this->savant->assign('weight', $weight);
288                 $this->savant->assign('type',   _AT($this->sNameVar));
289                 $this->savant->assign('number', TestQuestionCounter());
290                 $this->savant->display('test_questions/header.tmpl.php');
291         }
292
293         /**
294         * print the question template footer
295         */
296         /*final private */function displayFooter() {
297                 $this->savant->display('test_questions/footer.tmpl.php');
298         }
299
300         /**
301         * return only the non-empty choices from $row.
302         * assumes choices are sequential.
303         */
304         /*protected */function getChoices($row) {
305                 $choices = array();
306                 for ($i=0; $i < 10; $i++) {
307                         if ($row['choice_'.$i] != '') {
308                                 $num_choices++;
309                                 $choices[] = $row['choice_'.$i];
310                         } else {
311                                 break;
312                         }
313                 }
314                 return $choices;
315         }
316 }
317
318 /**
319 * orderingQuestion
320 *
321 */
322 class OrderingQuestion extends AbstractTestQuestion {
323         /*protected */ var $sNameVar = 'test_ordering';
324         /*protected */ var $sPrefix = 'ordering';
325         
326         /*protected */function assignDisplayResultVariables($row, $answer_row) {
327                 $answers = explode('|', $answer_row['answer']);
328
329                 $num_choices = count($this->getChoices($row));
330
331                 $this->savant->assign('base_href', AT_BASE_HREF);
332                 $this->savant->assign('num_choices', $num_choices);
333                 $this->savant->assign('answers', $answers);
334                 $this->savant->assign('row', $row);
335         }
336
337         /*protected */function assignQTIVariables($row) {
338                 $choices = $this->getChoices($row);
339                 $num_choices = count($choices);
340
341                 $this->savant->assign('num_choices', $num_choices);
342                 $this->savant->assign('row', $row);
343         }
344
345         /*protected */function assignDisplayVariables($row) {
346                 // determine the number of choices this question has
347                 // and save those choices to be re-assigned back to $row
348                 // in the randomized order.
349                 $choices = $this->getChoices($row);
350                 $num_choices = count($choices);
351
352                 // randomize the order of choices and re-assign to $row
353                 $this->seed($row['question_id']);
354                 $rand = array_rand($choices, $num_choices);
355                 for ($i=0; $i < 10; $i++) {
356                         $row['choice_'.$i] = $choices[$rand[$i]];
357                 }
358
359                 $this->savant->assign('num_choices', $num_choices);
360                 $this->savant->assign('row', $row);
361         }
362
363         /*protected */function assignDisplayStatisticsVariables($row, $answers) {
364                 $num_results = 0;               
365                 foreach ($answers as $answer) {
366                         $num_results += $answer['count'];
367                 }
368
369                 $choices = $this->getChoices($row);
370                 $num_choices = count($choices);
371
372                 $final_answers = array(); // assoc array of # of times that key was used correctly 0, 1, ...  $num -1
373                 foreach ($answers as $key => $value) {
374                         $values = explode('|', $key);
375                         // we assume $values is never empty and contains $num number of answers
376                         for ($i=0; $i<=$num_choices; $i++) {
377                                 if ($values[$i] == $i) {
378                                         $final_answers[$i] += $answers[$key]['count'];
379                                 }
380                         }
381                 }
382
383                 $this->savant->assign('num_results', $num_results);
384                 $this->savant->assign('num_choices', $num_choices);
385                 $this->savant->assign('answers', $final_answers);
386                 $this->savant->assign('row', $row);
387         }
388
389         /*public */function mark($row) { 
390                 $this->seed($row['question_id']);
391                 $num_choices = count($_POST['answers'][$row['question_id']]);
392                 $answers = range(0, $num_choices-1);
393                 $answers = array_rand($answers, $num_choices);
394
395                 $num_answer_correct = 0;
396
397                 $ordered_answers = array();
398
399                 for ($i = 0; $i < $num_choices ; $i++) {
400                         $_POST['answers'][$row['question_id']][$i] = intval($_POST['answers'][$row['question_id']][$i]);
401
402                         if ($_POST['answers'][$row['question_id']][$i] == -1) {
403                                 // nothing to do. it was left blank
404                         } else if ($_POST['answers'][$row['question_id']][$i] == $answers[$i]) {
405                                 $num_answer_correct++;
406                         }
407                         $ordered_answers[$answers[$i]] = $_POST['answers'][$row['question_id']][$i];
408                 }
409                 ksort($ordered_answers);
410
411                 $score = 0;
412
413                 // to avoid roundoff errors:
414                 if ($num_answer_correct == $num_choices) {
415                         $score = $row['weight'];
416                 } else if ($num_answer_correct > 0) {
417                         $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
418                         if ( (float) (int) $score == $score) {
419                                 $score = (int) $score; // a whole number with decimals, eg. "2.00"
420                         } else {
421                                 $score = trim($score, '0'); // remove trailing zeros, if any, eg. "2.50"
422                         }
423                 }
424
425                 $_POST['answers'][$row['question_id']] = implode('|', $ordered_answers);
426
427                 return $score;
428         }
429 }
430
431 /**
432 * truefalseQuestion
433 *
434 */
435 class TruefalseQuestion extends AbstracttestQuestion {
436         /*protected */ var $sPrefix = 'truefalse';
437         /*protected */ var $sNameVar   = 'test_tf';
438
439         /*protected */function assignQTIVariables($row) {
440                 $this->savant->assign('row', $row);
441         }
442
443         /*protected */function assignDisplayResultVariables($row, $answer_row) {
444
445                 $this->savant->assign('base_href', AT_BASE_HREF);
446                 $this->savant->assign('answers', $answer_row['answer']);
447                 $this->savant->assign('row', $row);
448         }
449
450         /*protected */function assignDisplayVariables($row) {
451                 $this->savant->assign('row', $row);
452         }
453
454         /*protected */function assignDisplayStatisticsVariables($row, $answers) {
455                 $num_results = 0;               
456                 foreach ($answers as $answer) {
457                         $num_results += $answer['count'];
458                 }
459
460                 $this->savant->assign('num_results', $num_results);
461                 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
462                 $this->savant->assign('num_true', (int) $answers['1']['count']);
463                 $this->savant->assign('num_false', (int) $answers['2']['count']);
464                 $this->savant->assign('row', $row);
465         }
466
467         /*public */function mark($row) { 
468                 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
469
470                 if ($row['answer_0'] == $_POST['answers'][$row['question_id']]) {
471                         return (int) $row['weight'];
472                 } // else:
473                 return 0;
474         }
475 }
476
477 /**
478 * likertQuestion
479 *
480 */
481 class LikertQuestion extends AbstracttestQuestion {
482         /*protected */ var $sPrefix = 'likert';
483         /*protected */ var $sNameVar   = 'test_lk';
484
485         /*protected */function assignQTIVariables($row) {
486                 $choices = $this->getChoices($row);
487                 $num_choices = count($choices);
488
489                 $this->savant->assign('num_choices', $num_choices);
490                 $this->savant->assign('row', $row);
491         }
492
493         /*protected */function assignDisplayResultVariables($row, $answer_row) {
494                 $this->savant->assign('answer', $answer_row['answer']);
495                 $this->savant->assign('row', $row);
496         }
497
498         /*protected */function assignDisplayVariables($row) {
499                 $choices = $this->getChoices($row);
500                 $num_choices = count($choices);
501
502                 $this->savant->assign('num_choices', $num_choices);
503                 $this->savant->assign('row', $row);
504         }
505
506         /*protected */function assignDisplayStatisticsVariables($row, $answers) {
507                 $num_results = 0;               
508                 foreach ($answers as $answer) {
509                         $num_results += $answer['count'];
510                 }
511                 
512                 $choices = $this->getChoices($row);
513                 $num_choices = count($choices);
514
515                 $sum = 0;
516                 for ($i=0; $i<$num_choices; $i++) {
517                         $sum += ($i+1) * $answers[$i]['count'];
518                 }
519                 $average = round($sum/$num_results, 1);
520
521                 $this->savant->assign('num_results', $num_results);
522                 $this->savant->assign('average', $average);
523                 $this->savant->assign('num_choices', $num_choices);
524                 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
525                 $this->savant->assign('answers', $answers);
526                 $this->savant->assign('row', $row);
527         }
528
529         /*public */function mark($row) { 
530                 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
531                 return 0;
532         }
533 }
534
535 /**
536 * longQuestion
537 *
538 */
539 class LongQuestion extends AbstracttestQuestion {
540         /*protected */ var $sPrefix = 'long';
541         /*protected */ var $sNameVar = 'test_open';
542
543         /*protected */function assignQTIVariables($row) {
544                 $this->savant->assign('row', $row);
545         }
546
547         /*protected */function assignDisplayResultVariables($row, $answer_row) {
548                 $this->savant->assign('answer', $answer_row['answer']);
549                 $this->savant->assign('row', $row);
550         }
551
552         /*protected */function assignDisplayVariables($row) {
553                 $this->savant->assign('row', $row);
554         }
555
556         /*protected */function assignDisplayStatisticsVariables($row, $answers) {
557                 $num_results = 0;               
558                 foreach ($answers as $answer) {
559                         $num_results += $answer['count'];
560                 }
561                 
562                 $this->savant->assign('num_results', $num_results);
563                 $this->savant->assign('num_blanks', (int) $answers['']['count']);
564                 $this->savant->assign('answers', $answers);
565                 $this->savant->assign('row', $row);
566         }
567
568         /*public */function mark($row) { 
569                 global $addslashes;
570                 $_POST['answers'][$row['question_id']] = $addslashes($_POST['answers'][$row['question_id']]);
571                 return 0;
572         }
573 }
574
575 /**
576 * matchingQuestion
577 *
578 */
579 class MatchingQuestion extends AbstracttestQuestion {
580         /*protected */ var $sPrefix = 'matching';
581         /*protected */ var $sNameVar   = 'test_matching';
582
583         /*protected */function assignQTIVariables($row) {
584                 $choices = $this->getChoices($row);
585                 $num_choices = count($choices);
586
587                 $num_options = 0;
588                 for ($i=0; $i < 10; $i++) {
589                         if ($row['option_'. $i] != '') {
590                                 $num_options++;
591                         }
592                 }
593
594                 $this->savant->assign('num_choices', $num_choices);
595                 $this->savant->assign('num_options', $num_options);
596                 $this->savant->assign('row', $row);
597         }
598
599         /*protected */function assignDisplayResultVariables($row, $answer_row) {
600                 $num_options = 0;
601                 for ($i=0; $i < 10; $i++) {
602                         if ($row['option_'. $i] != '') {
603                                 $num_options++;
604                         }
605                 }
606
607                 $answer_row['answer'] = explode('|', $answer_row['answer']);
608
609                 global $_letters;
610
611                 $this->savant->assign('base_href', AT_BASE_HREF);
612                 $this->savant->assign('answers', $answer_row['answer']);
613                 $this->savant->assign('letters', $_letters);
614                 $this->savant->assign('num_options', $num_options);
615                 $this->savant->assign('row', $row);
616         }
617
618         /*protected */function assignDisplayVariables($row) {
619                 $choices = $this->getChoices($row);
620                 $num_choices = count($choices);
621
622                 $num_options = 0;
623                 for ($i=0; $i < 10; $i++) {
624                         if ($row['option_'. $i] != '') {
625                                 $num_options++;
626                         }
627                 }
628                 
629                 global $_letters;
630
631                 $this->savant->assign('num_choices', $num_choices);
632                 $this->savant->assign('base_href', AT_BASE_HREF);
633                 $this->savant->assign('letters', $_letters);
634                 $this->savant->assign('num_options', $num_options);
635                 $this->savant->assign('row', $row);
636         }
637
638         /*protected */function assignDisplayStatisticsVariables($row, $answers) {
639                 $choices = $this->getChoices($row);
640                 $num_choices = count($choices);
641
642                 $num_results = 0;
643                 foreach ($answers as $answer) {
644                         $num_results += $answer['count'];
645                 }
646                                         
647                 foreach ($answers as $key => $value) {
648                         $values = explode('|', $key);
649                         if (count($values) > 1) {
650                                 for ($i=0; $i<count($values); $i++) {
651                                         $answers[$values[$i]]['count']++;
652                                 }
653                         }
654                 }
655
656                 $this->savant->assign('num_choices', $num_choices);
657                 $this->savant->assign('num_results', $num_results);
658                 $this->savant->assign('answers', $answers);
659                 $this->savant->assign('row', $row);
660         }
661
662         /*public */function mark($row) { 
663                 $num_choices = count($_POST['answers'][$row['question_id']]);
664                 $num_answer_correct = 0;
665                 foreach ($_POST['answers'][$row['question_id']] as $item_id => $response) {
666                         if ($row['answer_' . $item_id] == $response) {
667                                 $num_answer_correct++;
668                         }
669                         $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
670                 }
671
672                 $score = 0;
673                 // to avoid roundoff errors:
674                 if ($num_answer_correct == $num_choices) {
675                         $score = $row['weight'];
676                 } else if ($num_answer_correct > 0) {
677                         $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
678                         if ( (float) (int) $score == $score) {
679                                 $score = (int) $score; // a whole number with decimals, eg. "2.00"
680                         } else {
681                                 $score = trim($score, '0'); // remove trailing zeros, if any
682                         }
683                 }
684
685                 $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
686
687                 return $score;
688         }
689 }
690
691 /**
692 * matchingddQuestion
693 *
694 */
695 class MatchingddQuestion extends MatchingQuestion {
696         /*protected */ var $sPrefix = 'matchingdd';
697         /*protected */ var $sNameVar   = 'test_matchingdd';
698 }
699
700 /**
701 * multichoiceQuestion
702 *
703 */
704 class MultichoiceQuestion extends AbstracttestQuestion {
705         /*protected */ var $sPrefix = 'multichoice';
706         /*protected */var $sNameVar = 'test_mc';
707
708         /*protected */function assignQTIVariables($row) {
709                 $choices = $this->getChoices($row);
710                 $num_choices = count($choices);
711
712                 $this->savant->assign('num_choices', $num_choices);
713                 $this->savant->assign('row', $row);
714         }
715
716         /*protected */function assignDisplayResultVariables($row, $answer_row) {
717                 if (array_sum(array_slice($row, 16, -6)) > 1) {
718                         $answer_row['answer'] = explode('|', $answer_row['answer']);
719                 } else {
720                         $answer_row['answer'] = array($answer_row['answer']);
721                 }
722
723
724                 $this->savant->assign('base_href', AT_BASE_HREF);
725                 $this->savant->assign('answers', $answer_row['answer']);
726                 $this->savant->assign('row', $row);
727         }
728
729         /*protected */function assignDisplayVariables($row) {
730                 $choices = $this->getChoices($row);
731                 $num_choices = count($choices);
732
733                 $this->savant->assign('num_choices', $num_choices);
734                 $this->savant->assign('row', $row);
735         }
736
737         /*protected */function assignDisplayStatisticsVariables($row, $answers) {
738                 $choices = $this->getChoices($row);
739                 $num_choices = count($choices);
740
741                 $num_results = 0;
742                 foreach ($answers as $answer) {
743                         $num_results += $answer['count'];
744                 }
745                                         
746                 foreach ($answers as $key => $value) {
747                         $values = explode('|', $key);
748                         if (count($values) > 1) {
749                                 for ($i=0; $i<count($values); $i++) {
750                                         $answers[$values[$i]]['count']++;
751                                 }
752                         }
753                 }
754
755                 $this->savant->assign('num_choices', $num_choices);
756                 $this->savant->assign('num_results', $num_results);
757                 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
758                 $this->savant->assign('answers', $answers);
759                 $this->savant->assign('row', $row);
760         }
761
762         /*public */function mark($row) { 
763                 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
764                 if ($row['answer_' . $_POST['answers'][$row['question_id']]]) {
765                         $score = $row['weight'];
766                 } else if ($_POST['answers'][$row['question_id']] == -1) {
767                         $has_answer = 0;
768                         for($i=0; $i<10; $i++) {
769                                 $has_answer += $row['answer_'.$i];
770                         }
771                         if (!$has_answer && $row['weight']) {
772                                 // If MC has no answer and user answered "leave blank"
773                                 $score = $row['weight'];
774                         }
775                 }
776                 return $score;
777         }
778 }
779
780 /**
781 * multianswerQuestion
782 *
783 */
784 class MultianswerQuestion extends MultichoiceQuestion {
785         /*protected */ var $sPrefix  = 'multianswer';
786         /*protected */ var $sNameVar = 'test_ma';
787
788         /*public */function mark($row) { 
789                 $num_correct = array_sum(array_slice($row, 3));
790
791                 if (is_array($_POST['answers'][$row['question_id']]) && count($_POST['answers'][$row['question_id']]) > 1) {
792                         if (($i = array_search('-1', $_POST['answers'][$row['question_id']])) !== FALSE) {
793                                 unset($_POST['answers'][$row['question_id']][$i]);
794                         }
795                         $num_answer_correct = 0;
796                         foreach ($_POST['answers'][$row['question_id']] as $item_id => $answer) {
797                                 if ($row['answer_' . $answer]) {
798                                         // correct answer
799                                         $num_answer_correct++;
800                                 } else {
801                                         // wrong answer
802                                         $num_answer_correct--;
803                                 }
804                                 $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
805                         }
806                         if ($num_answer_correct == $num_correct) {
807                                 $score = $row['weight'];
808                         } else {
809                                 $score = 0;
810                         }
811                         $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
812                 } else {
813                         // no answer given
814                         $_POST['answers'][$row['question_id']] = '-1'; // left blank
815                         $score = 0;
816                 }
817                 return $score;
818         }
819 }
820 ?>