MDL-53966 lesson: Allow maximum number of attempts to be unlimited
[moodle.git] / mod / lesson / pagetypes / multichoice.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Multichoice
20  *
21  * @package mod_lesson
22  * @copyright  2009 Sam Hemelryk
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  **/
26 defined('MOODLE_INTERNAL') || die();
28 /** Multichoice question type */
29 define("LESSON_PAGE_MULTICHOICE",   "3");
31 class lesson_page_type_multichoice extends lesson_page {
33     protected $type = lesson_page::TYPE_QUESTION;
34     protected $typeidstring = 'multichoice';
35     protected $typeid = LESSON_PAGE_MULTICHOICE;
36     protected $string = null;
38     public function get_typeid() {
39         return $this->typeid;
40     }
41     public function get_typestring() {
42         if ($this->string===null) {
43             $this->string = get_string($this->typeidstring, 'lesson');
44         }
45         return $this->string;
46     }
47     public function get_idstring() {
48         return $this->typeidstring;
49     }
51     /**
52      * Gets an array of the jumps used by the answers of this page
53      *
54      * @return array
55      */
56     public function get_jumps() {
57         global $DB;
58         $jumps = array();
59         if ($answers = $this->get_answers()) {
60             foreach ($answers as $answer) {
61                 if ($answer->answer === '') {
62                     // show only jumps for real branches (==have description)
63                     continue;
64                 }
65                 $jumps[] = $this->get_jump_name($answer->jumpto);
66             }
67         } else {
68             // We get here is the lesson was created on a Moodle 1.9 site and
69             // the lesson contains question pages without any answers.
70             $jumps[] = $this->get_jump_name($this->properties->nextpageid);
71         }
72         return $jumps;
73     }
75     public function get_used_answers() {
76         $answers = $this->get_answers();
77         foreach ($answers as $key=>$answer) {
78             if ($answer->answer === '') {
79                 unset($answers[$key]);
80             } else {
81                 $answers[$key] = parent::rewrite_answers_urls($answer);
82             }
83         }
84         return $answers;
85     }
87     public function display($renderer, $attempt) {
88         global $CFG, $PAGE;
89         $answers = $this->get_used_answers();
90         shuffle($answers);
91         $action = $CFG->wwwroot.'/mod/lesson/continue.php';
92         $params = array('answers'=>$answers, 'lessonid'=>$this->lesson->id, 'contents'=>$this->get_contents(), 'attempt'=>$attempt);
93         if ($this->properties->qoption) {
94             $mform = new lesson_display_answer_form_multichoice_multianswer($action, $params);
95         } else {
96             $mform = new lesson_display_answer_form_multichoice_singleanswer($action, $params);
97         }
98         $data = new stdClass;
99         $data->id = $PAGE->cm->id;
100         $data->pageid = $this->properties->id;
101         $mform->set_data($data);
103         // Trigger an event question viewed.
104         $eventparams = array(
105             'context' => context_module::instance($PAGE->cm->id),
106             'objectid' => $this->properties->id,
107             'other' => array(
108                     'pagetype' => $this->get_typestring()
109                 )
110             );
112         $event = \mod_lesson\event\question_viewed::create($eventparams);
113         $event->trigger();
114         return $mform->display();
115     }
117     public function check_answer() {
118         global $DB, $CFG, $PAGE;
119         $result = parent::check_answer();
121         $formattextdefoptions = new stdClass();
122         $formattextdefoptions->noclean = true;
123         $formattextdefoptions->para = false;
125         $answers = $this->get_used_answers();
126         shuffle($answers);
127         $action = $CFG->wwwroot.'/mod/lesson/continue.php';
128         $params = array('answers'=>$answers, 'lessonid'=>$this->lesson->id, 'contents'=>$this->get_contents());
129         if ($this->properties->qoption) {
130             $mform = new lesson_display_answer_form_multichoice_multianswer($action, $params);
131         } else {
132             $mform = new lesson_display_answer_form_multichoice_singleanswer($action, $params);
133         }
134         $data = $mform->get_data();
135         require_sesskey();
137         if (!$data) {
138             $result->inmediatejump = true;
139             $result->newpageid = $this->properties->id;
140             return $result;
141         }
143         if ($this->properties->qoption) {
144             // Multianswer allowed, user's answer is an array
146             if (empty($data->answer) || !is_array($data->answer)) {
147                 $result->noanswer = true;
148                 return $result;
149             }
151             $studentanswers = array();
152             foreach ($data->answer as $key=>$value) {
153                 $studentanswers[] = (int)$key;
154             }
156             // get what the user answered
157             $result->userresponse = implode(",", $studentanswers);
159             // get the answers in a set order, the id order
160             $answers = $this->get_used_answers();
161             $ncorrect = 0;
162             $nhits = 0;
163             $responses = array();
164             $correctanswerid = 0;
165             $wronganswerid = 0;
166             // store student's answers for displaying on feedback page
167             $result->studentanswer = '';
168             $result->studentanswerformat = FORMAT_HTML;
169             foreach ($answers as $answer) {
170                 foreach ($studentanswers as $answerid) {
171                     if ($answerid == $answer->id) {
172                         $studentanswerarray[] = format_text($answer->answer, $answer->answerformat, $formattextdefoptions);
173                         $responses[$answerid] = format_text($answer->response, $answer->responseformat, $formattextdefoptions);
174                     }
175                 }
176             }
177             $result->studentanswer = implode(self::MULTIANSWER_DELIMITER, $studentanswerarray);
178             $correctpageid = null;
179             $wrongpageid = null;
181             // Iterate over all the possible answers.
182             foreach ($answers as $answer) {
183                 if ($this->lesson->custom) {
184                     $iscorrectanswer = $answer->score > 0;
185                 } else {
186                     $iscorrectanswer = $this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto);
187                 }
189                 // Iterate over all the student answers to check if he selected the current possible answer.
190                 foreach ($studentanswers as $answerid) {
191                     if ($answerid == $answer->id) {
192                         if ($iscorrectanswer) {
193                             $nhits++;
194                         } else {
195                             // Always jump to the page related to the student's first wrong answer.
196                             if (!isset($wrongpageid)) {
197                                 // Leave in its "raw" state - will be converted into a proper page id later.
198                                 $wrongpageid = $answer->jumpto;
199                             }
200                             // Save the answer id for scoring.
201                             if ($wronganswerid == 0) {
202                                 $wronganswerid = $answer->id;
203                             }
204                         }
205                     }
206                 }
208                 if ($iscorrectanswer) {
209                     $ncorrect++;
211                     // Save the first jumpto page id, may be needed!
212                     if (!isset($correctpageid)) {
213                         // Leave in its "raw" state - will be converted into a proper page id later.
214                         $correctpageid = $answer->jumpto;
215                     }
216                     // Save the answer id for scoring.
217                     if ($correctanswerid == 0) {
218                         $correctanswerid = $answer->id;
219                     }
220                 }
221             }
223             if ((count($studentanswers) == $ncorrect) and ($nhits == $ncorrect)) {
224                 $result->correctanswer = true;
225                 $result->response  = implode(self::MULTIANSWER_DELIMITER, $responses);
226                 $result->newpageid = $correctpageid;
227                 $result->answerid  = $correctanswerid;
228             } else {
229                 $result->response  = implode(self::MULTIANSWER_DELIMITER, $responses);
230                 $result->newpageid = $wrongpageid;
231                 $result->answerid  = $wronganswerid;
232             }
233         } else {
234             // only one answer allowed
235             if (!isset($data->answerid) || (empty($data->answerid) && !is_int($data->answerid))) {
236                 $result->noanswer = true;
237                 return $result;
238             }
239             $result->answerid = $data->answerid;
240             if (!$answer = $DB->get_record("lesson_answers", array("id" => $result->answerid))) {
241                 print_error("Continue: answer record not found");
242             }
243             $answer = parent::rewrite_answers_urls($answer);
244             if ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) {
245                 $result->correctanswer = true;
246             }
247             if ($this->lesson->custom) {
248                 if ($answer->score > 0) {
249                     $result->correctanswer = true;
250                 } else {
251                     $result->correctanswer = false;
252                 }
253             }
254             $result->newpageid = $answer->jumpto;
255             $result->response  = format_text($answer->response, $answer->responseformat, $formattextdefoptions);
256             $result->userresponse = format_text($answer->answer, $answer->answerformat, $formattextdefoptions);
257             $result->studentanswer = $result->userresponse;
258         }
259         return $result;
260     }
262     public function option_description_string() {
263         if ($this->properties->qoption) {
264             return " - ".get_string("multianswer", "lesson");
265         }
266         return parent::option_description_string();
267     }
269     public function display_answers(html_table $table) {
270         $answers = $this->get_used_answers();
271         $options = new stdClass;
272         $options->noclean = true;
273         $options->para = false;
274         $i = 1;
275         foreach ($answers as $answer) {
276             $answer = parent::rewrite_answers_urls($answer);
277             $cells = array();
278             if ($this->lesson->custom && $answer->score > 0) {
279                 // if the score is > 0, then it is correct
280                 $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . " {$i}</label>: \n";
281             } else if ($this->lesson->custom) {
282                 $cells[] = '<label>' . get_string('answer', 'lesson') . " {$i}</label>: \n";
283             } else if ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) {
284                 // underline correct answers
285                 $cells[] = '<span class="correct">' . get_string('answer', 'lesson') . " {$i}</span>: \n";
286             } else {
287                 $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . " {$i}</label>: \n";
288             }
289             $cells[] = format_text($answer->answer, $answer->answerformat, $options);
290             $table->data[] = new html_table_row($cells);
292             $cells = array();
293             $cells[] = '<label>' . get_string('response', 'lesson') . " {$i} </label>:\n";
294             $cells[] = format_text($answer->response, $answer->responseformat, $options);
295             $table->data[] = new html_table_row($cells);
297             $cells = array();
298             $cells[] = '<label>' . get_string('score', 'lesson') . '</label>:';
299             $cells[] = $answer->score;
300             $table->data[] = new html_table_row($cells);
302             $cells = array();
303             $cells[] = '<label>' . get_string('jump', 'lesson') . '</label>:';
304             $cells[] = $this->get_jump_name($answer->jumpto);
305             $table->data[] = new html_table_row($cells);
306             if ($i === 1){
307                 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
308             }
309             $i++;
310         }
311         return $table;
312     }
313     public function stats(array &$pagestats, $tries) {
314         $temp = $this->lesson->get_last_attempt($tries);
315         if ($this->properties->qoption) {
316             $userresponse = explode(",", $temp->useranswer);
317             foreach ($userresponse as $response) {
318                 if (isset($pagestats[$temp->pageid][$response])) {
319                     $pagestats[$temp->pageid][$response]++;
320                 } else {
321                     $pagestats[$temp->pageid][$response] = 1;
322                 }
323             }
324         } else {
325             if (isset($pagestats[$temp->pageid][$temp->answerid])) {
326                 $pagestats[$temp->pageid][$temp->answerid]++;
327             } else {
328                 $pagestats[$temp->pageid][$temp->answerid] = 1;
329             }
330         }
331         if (isset($pagestats[$temp->pageid]["total"])) {
332             $pagestats[$temp->pageid]["total"]++;
333         } else {
334             $pagestats[$temp->pageid]["total"] = 1;
335         }
336         return true;
337     }
339     public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
340         $answers = $this->get_used_answers();
341         $formattextdefoptions = new stdClass;
342         $formattextdefoptions->para = false;  //I'll use it widely in this page
343         $formattextdefoptions->context = $answerpage->context;
345         foreach ($answers as $answer) {
346             if ($this->properties->qoption) {
347                 if ($useranswer == null) {
348                     $userresponse = array();
349                 } else {
350                     $userresponse = explode(",", $useranswer->useranswer);
351                 }
352                 if (in_array($answer->id, $userresponse)) {
353                     // make checked
354                     $data = "<input  readonly=\"readonly\" disabled=\"disabled\" name=\"answer[$i]\" checked=\"checked\" type=\"checkbox\" value=\"1\" />";
355                     if (!isset($answerdata->response)) {
356                         if ($answer->response == null) {
357                             if ($useranswer->correct) {
358                                 $answerdata->response = get_string("thatsthecorrectanswer", "lesson");
359                             } else {
360                                 $answerdata->response = get_string("thatsthewronganswer", "lesson");
361                             }
362                         } else {
363                             $answerdata->response = $answer->response;
364                         }
365                     }
366                     if (!isset($answerdata->score)) {
367                         if ($this->lesson->custom) {
368                             $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
369                         } elseif ($useranswer->correct) {
370                             $answerdata->score = get_string("receivedcredit", "lesson");
371                         } else {
372                             $answerdata->score = get_string("didnotreceivecredit", "lesson");
373                         }
374                     }
375                 } else {
376                     // unchecked
377                     $data = "<input type=\"checkbox\" readonly=\"readonly\" name=\"answer[$i]\" value=\"0\" disabled=\"disabled\" />";
378                 }
379                 if (($answer->score > 0 && $this->lesson->custom) || ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto) && !$this->lesson->custom)) {
380                     $data = "<div class=highlight>".$data.' '.format_text($answer->answer,$answer->answerformat,$formattextdefoptions)."</div>";
381                 } else {
382                     $data .= format_text($answer->answer,$answer->answerformat,$formattextdefoptions);
383                 }
384             } else {
385                 if ($useranswer != null and $answer->id == $useranswer->answerid) {
386                     // make checked
387                     $data = "<input  readonly=\"readonly\" disabled=\"disabled\" name=\"answer[$i]\" checked=\"checked\" type=\"checkbox\" value=\"1\" />";
388                     if ($answer->response == null) {
389                         if ($useranswer->correct) {
390                             $answerdata->response = get_string("thatsthecorrectanswer", "lesson");
391                         } else {
392                             $answerdata->response = get_string("thatsthewronganswer", "lesson");
393                         }
394                     } else {
395                         $answerdata->response = $answer->response;
396                     }
397                     if ($this->lesson->custom) {
398                         $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
399                     } elseif ($useranswer->correct) {
400                         $answerdata->score = get_string("receivedcredit", "lesson");
401                     } else {
402                         $answerdata->score = get_string("didnotreceivecredit", "lesson");
403                     }
404                 } else {
405                     // unchecked
406                     $data = "<input type=\"checkbox\" readonly=\"readonly\" name=\"answer[$i]\" value=\"0\" disabled=\"disabled\" />";
407                 }
408                 if (($answer->score > 0 && $this->lesson->custom) || ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto) && !$this->lesson->custom)) {
409                     $data = "<div class=\"highlight\">".$data.' '.format_text($answer->answer,FORMAT_MOODLE,$formattextdefoptions)."</div>";
410                 } else {
411                     $data .= format_text($answer->answer,$answer->answerformat,$formattextdefoptions);
412                 }
413             }
414             if (isset($pagestats[$this->properties->id][$answer->id])) {
415                 $percent = $pagestats[$this->properties->id][$answer->id] / $pagestats[$this->properties->id]["total"] * 100;
416                 $percent = round($percent, 2);
417                 $percent .= "% ".get_string("checkedthisone", "lesson");
418             } else {
419                 $percent = get_string("noonecheckedthis", "lesson");
420             }
422             $answerdata->answers[] = array($data, $percent);
423             $answerpage->answerdata = $answerdata;
424         }
425         return $answerpage;
426     }
430 class lesson_add_page_form_multichoice extends lesson_add_page_form_base {
432     public $qtype = 'multichoice';
433     public $qtypestring = 'multichoice';
434     protected $answerformat = LESSON_ANSWER_HTML;
435     protected $responseformat = LESSON_ANSWER_HTML;
437     public function custom_definition() {
439         $this->_form->addElement('checkbox', 'qoption', get_string('options', 'lesson'), get_string('multianswer', 'lesson'));
440         $this->_form->setDefault('qoption', 0);
441         $this->_form->addHelpButton('qoption', 'multianswer', 'lesson');
443         for ($i = 0; $i < $this->_customdata['lesson']->maxanswers; $i++) {
444             $this->_form->addElement('header', 'answertitle'.$i, get_string('answer').' '.($i+1));
445             $this->add_answer($i, null, ($i<2), $this->get_answer_format());
446             $this->add_response($i);
447             $this->add_jumpto($i, null, ($i == 0 ? LESSON_NEXTPAGE : LESSON_THISPAGE));
448             $this->add_score($i, null, ($i===0)?1:0);
449         }
450     }
453 class lesson_display_answer_form_multichoice_singleanswer extends moodleform {
455     public function definition() {
456         global $USER, $OUTPUT;
457         $mform = $this->_form;
458         $answers = $this->_customdata['answers'];
459         $lessonid = $this->_customdata['lessonid'];
460         $contents = $this->_customdata['contents'];
461         if (array_key_exists('attempt', $this->_customdata)) {
462             $attempt = $this->_customdata['attempt'];
463         } else {
464             $attempt = new stdClass();
465             $attempt->answerid = null;
466         }
468         // Disable shortforms.
469         $mform->setDisableShortforms();
471         $mform->addElement('header', 'pageheader');
473         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
475         $hasattempt = false;
476         $disabled = '';
477         if (isset($USER->modattempts[$lessonid]) && !empty($USER->modattempts[$lessonid])) {
478             $hasattempt = true;
479             $disabled = array('disabled' => 'disabled');
480         }
482         $options = new stdClass;
483         $options->para = false;
484         $options->noclean = true;
486         $mform->addElement('hidden', 'id');
487         $mform->setType('id', PARAM_INT);
489         $mform->addElement('hidden', 'pageid');
490         $mform->setType('pageid', PARAM_INT);
492         $i = 0;
493         foreach ($answers as $answer) {
494             $mform->addElement('html', '<div class="answeroption">');
495             $answer->answer = preg_replace('#>$#', '> ', $answer->answer);
496             $mform->addElement('radio','answerid',null,format_text($answer->answer, $answer->answerformat, $options),$answer->id, $disabled);
497             $mform->setType('answer'.$i, PARAM_INT);
498             if ($hasattempt && $answer->id == $USER->modattempts[$lessonid]->answerid) {
499                 $mform->setDefault('answerid', $USER->modattempts[$lessonid]->answerid);
500             }
501             $mform->addElement('html', '</div>');
502             $i++;
503         }
505         if ($hasattempt) {
506             $this->add_action_buttons(null, get_string("nextpage", "lesson"));
507         } else {
508             $this->add_action_buttons(null, get_string("submit", "lesson"));
509         }
510     }
514 class lesson_display_answer_form_multichoice_multianswer extends moodleform {
516     public function definition() {
517         global $USER, $OUTPUT;
518         $mform = $this->_form;
519         $answers = $this->_customdata['answers'];
521         $lessonid = $this->_customdata['lessonid'];
522         $contents = $this->_customdata['contents'];
524         // Disable shortforms.
525         $mform->setDisableShortforms();
527         $mform->addElement('header', 'pageheader');
529         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
531         $hasattempt = false;
532         $disabled = '';
533         $useranswers = array();
534         if (isset($USER->modattempts[$lessonid]) && !empty($USER->modattempts[$lessonid])) {
535             $hasattempt = true;
536             $disabled = array('disabled' => 'disabled');
537             $useranswers = explode(',', $USER->modattempts[$lessonid]->useranswer);
538         }
540         $options = new stdClass;
541         $options->para = false;
542         $options->noclean = true;
544         $mform->addElement('hidden', 'id');
545         $mform->setType('id', PARAM_INT);
547         $mform->addElement('hidden', 'pageid');
548         $mform->setType('pageid', PARAM_INT);
550         foreach ($answers as $answer) {
551             $mform->addElement('html', '<div class="answeroption">');
552             $answerid = 'answer['.$answer->id.']';
553             if ($hasattempt && in_array($answer->id, $useranswers)) {
554                 $answerid = 'answer_'.$answer->id;
555                 $mform->addElement('hidden', 'answer['.$answer->id.']', $answer->answer);
556                 $mform->setType('answer['.$answer->id.']', PARAM_NOTAGS);
557                 $mform->setDefault($answerid, true);
558                 $mform->setDefault('answer['.$answer->id.']', true);
559             }
560             // NOTE: our silly checkbox supports only value '1' - we can not use it like the radiobox above!!!!!!
561             $answer->answer = preg_replace('#>$#', '> ', $answer->answer);
562             $mform->addElement('checkbox', $answerid, null, format_text($answer->answer, $answer->answerformat, $options), $disabled);
563             $mform->setType($answerid, PARAM_INT);
565             $mform->addElement('html', '</div>');
566         }
568         if ($hasattempt) {
569             $this->add_action_buttons(null, get_string("nextpage", "lesson"));
570         } else {
571             $this->add_action_buttons(null, get_string("submit", "lesson"));
572         }
573     }