MDL-53966 lesson: Allow maximum number of attempts to be unlimited
[moodle.git] / mod / lesson / pagetypes / matching.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  * Matching
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 /** Matching question type */
29 define("LESSON_PAGE_MATCHING",      "5");
31 class lesson_page_type_matching extends lesson_page {
33     protected $type = lesson_page::TYPE_QUESTION;
34     protected $typeid = LESSON_PAGE_MATCHING;
35     protected $typeidstring = 'matching';
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     }
50     public function display($renderer, $attempt) {
51         global $USER, $CFG, $PAGE;
52         $mform = $this->make_answer_form($attempt);
53         $data = new stdClass;
54         $data->id = $PAGE->cm->id;
55         $data->pageid = $this->properties->id;
56         $mform->set_data($data);
58         // Trigger an event question viewed.
59         $eventparams = array(
60             'context' => context_module::instance($PAGE->cm->id),
61             'objectid' => $this->properties->id,
62             'other' => array(
63                     'pagetype' => $this->get_typestring()
64                 )
65             );
67         $event = \mod_lesson\event\question_viewed::create($eventparams);
68         $event->trigger();
69         return $mform->display();
70     }
72     protected function make_answer_form($attempt=null) {
73         global $USER, $CFG;
74         // don't shuffle answers (could be an option??)
75         $getanswers = array_slice($this->get_answers(), 2);
77         $answers = array();
78         foreach ($getanswers as $getanswer) {
79             $answers[$getanswer->id] = $getanswer;
80         }
82         $responses = array();
83         foreach ($answers as $answer) {
84             // get all the response
85             if ($answer->response != null) {
86                 $responses[] = trim($answer->response);
87             }
88         }
90         $responseoptions = array(''=>get_string('choosedots'));
91         if (!empty($responses)) {
92             shuffle($responses);
93             foreach ($responses as  $response) {
94                 $responseoptions[htmlspecialchars($response)] = $response;
95             }
96         }
97         if (isset($USER->modattempts[$this->lesson->id]) && !empty($attempt->useranswer)) {
98             $useranswers = explode(',', $attempt->useranswer);
99             $t = 0;
100         } else {
101             $useranswers = array();
102         }
104         $action = $CFG->wwwroot.'/mod/lesson/continue.php';
105         $params = array('answers'=>$answers, 'useranswers'=>$useranswers, 'responseoptions'=>$responseoptions, 'lessonid'=>$this->lesson->id, 'contents'=>$this->get_contents());
106         $mform = new lesson_display_answer_form_matching($action, $params);
107         return $mform;
108     }
110     public function create_answers($properties) {
111         global $DB, $PAGE;
112         // now add the answers
113         $newanswer = new stdClass;
114         $newanswer->lessonid = $this->lesson->id;
115         $newanswer->pageid = $this->properties->id;
116         $newanswer->timecreated = $this->properties->timecreated;
118         $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
119         $context = context_module::instance($cm->id);
121         // Check for duplicate response format.
122         $duplicateresponse = array();
123         if (is_array($properties->response_editor) &&             // If there are response_editors to iterate.
124                 is_array(reset($properties->response_editor))) {  // And they come split into text & format array.
125             foreach ($properties->response_editor as $response) { // Iterate over all them.
126                 $duplicateresponse[] = $response['text'];         // Picking the text only. This pagetype is that way.
127             }
128             $properties->response_editor = $duplicateresponse;
129         }
131         $answers = array();
133         // need to add two to offset correct response and wrong response
134         $this->lesson->maxanswers = $this->lesson->maxanswers + 2;
135         for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
136             $answer = clone($newanswer);
137             if (!empty($properties->answer_editor[$i]) && is_array($properties->answer_editor[$i])) {
138                 $answer->answer = $properties->answer_editor[$i]['text'];
139                 $answer->answerformat = $properties->answer_editor[$i]['format'];
140             }
141             if (!empty($properties->response_editor[$i])) {
142                 $answer->response = $properties->response_editor[$i];
143                 $answer->responseformat = 0;
144             }
146             if (isset($properties->jumpto[$i])) {
147                 $answer->jumpto = $properties->jumpto[$i];
148             }
149             if ($this->lesson->custom && isset($properties->score[$i])) {
150                 $answer->score = $properties->score[$i];
151             }
153             if (isset($answer->answer) && $answer->answer != '') {
154                 $answer->id = $DB->insert_record("lesson_answers", $answer);
155                 $this->save_answers_files($context, $PAGE->course->maxbytes,
156                         $answer, $properties->answer_editor[$i]);
157                 $answers[$answer->id] = new lesson_page_answer($answer);
158             } else if ($i < 2) {
159                 $answer->id = $DB->insert_record("lesson_answers", $answer);
160                 $answers[$answer->id] = new lesson_page_answer($answer);
161             } else {
162                 break;
163             }
164         }
165         $this->answers = $answers;
166         return $answers;
167     }
169     public function check_answer() {
170         global $CFG, $PAGE;
172         $formattextdefoptions = new stdClass();
173         $formattextdefoptions->noclean = true;
174         $formattextdefoptions->para = false;
176         $result = parent::check_answer();
178         $mform = $this->make_answer_form();
180         $data = $mform->get_data();
181         require_sesskey();
183         if (!$data) {
184             $result->inmediatejump = true;
185             $result->newpageid = $this->properties->id;
186             return $result;
187         }
189         $response = $data->response;
190         $getanswers = $this->get_answers();
191         foreach ($getanswers as $key => $answer) {
192             $getanswers[$key] = parent::rewrite_answers_urls($answer);
193         }
195         $correct = array_shift($getanswers);
196         $wrong   = array_shift($getanswers);
198         $answers = array();
199         foreach ($getanswers as $key => $answer) {
200             if ($answer->answer !== '' or $answer->response !== '') {
201                 $answers[$answer->id] = $answer;
202             }
203         }
205         // get the user's exact responses for record keeping
206         $hits = 0;
207         $userresponse = array();
208         $result->studentanswerformat = FORMAT_HTML;
209         foreach ($response as $id => $value) {
210             if ($value == '') {
211                 $result->noanswer = true;
212                 return $result;
213             }
214             $value = htmlspecialchars_decode($value);
215             $userresponse[] = $value;
216             // Make sure the user's answer exists in question's answer
217             if (array_key_exists($id, $answers)) {
218                 $answer = $answers[$id];
219                 $result->studentanswer .= '<br />'.format_text($answer->answer, $answer->answerformat, $formattextdefoptions).' = '.$value;
220                 if (trim($answer->response) == trim($value)) {
221                     $hits++;
222                 }
223             }
224         }
226         $result->userresponse = implode(",", $userresponse);
228         if ($hits == count($answers)) {
229             $result->correctanswer = true;
230             $result->response      = format_text($correct->answer, $correct->answerformat, $formattextdefoptions);
231             $result->answerid      = $correct->id;
232             $result->newpageid     = $correct->jumpto;
233         } else {
234             $result->correctanswer = false;
235             $result->response      = format_text($wrong->answer, $wrong->answerformat, $formattextdefoptions);
236             $result->answerid      = $wrong->id;
237             $result->newpageid     = $wrong->jumpto;
238         }
240         return $result;
241     }
243     public function option_description_string() {
244         return get_string("firstanswershould", "lesson");
245     }
247     public function display_answers(html_table $table) {
248         $answers = $this->get_answers();
249         $options = new stdClass;
250         $options->noclean = true;
251         $options->para = false;
252         $i = 1;
253         $n = 0;
255         foreach ($answers as $answer) {
256             $answer = parent::rewrite_answers_urls($answer);
257             if ($n < 2) {
258                 if ($answer->answer != null) {
259                     $cells = array();
260                     if ($n == 0) {
261                         $cells[] = '<label>' . get_string('correctresponse', 'lesson') . '</label>';
262                     } else {
263                         $cells[] = '<label>' . get_string('wrongresponse', 'lesson') . '</label>';
264                     }
265                     $cells[] = format_text($answer->answer, $answer->answerformat, $options);
266                     $table->data[] = new html_table_row($cells);
267                 }
269                 if ($n == 0) {
270                     $cells = array();
271                     $cells[] = '<label>' . get_string('correctanswerscore', 'lesson') . '</label>: ';
272                     $cells[] = $answer->score;
273                     $table->data[] = new html_table_row($cells);
275                     $cells = array();
276                     $cells[] = '<label>' . get_string('correctanswerjump', 'lesson') . '</label>: ';
277                     $cells[] = $this->get_jump_name($answer->jumpto);
278                     $table->data[] = new html_table_row($cells);
279                 } elseif ($n == 1) {
280                     $cells = array();
281                     $cells[] = '<label>' . get_string('wronganswerscore', 'lesson') . '</label>: ';
282                     $cells[] = $answer->score;
283                     $table->data[] = new html_table_row($cells);
285                     $cells = array();
286                     $cells[] = '<label>' . get_string('wronganswerjump', 'lesson') . '</label>: ';
287                     $cells[] = $this->get_jump_name($answer->jumpto);
288                     $table->data[] = new html_table_row($cells);
289                 }
291                 if ($n === 0){
292                     $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
293                 }
294                 $n++;
295                 $i--;
296             } else {
297                 $cells = array();
298                 if ($this->lesson->custom && $answer->score > 0) {
299                     // if the score is > 0, then it is correct
300                     $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . " {$i}</label>: \n";
301                 } else if ($this->lesson->custom) {
302                     $cells[] = '<label>' . get_string('answer', 'lesson') . " {$i}</label>: \n";
303                 } else if ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) {
304                     $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . " {$i}</label>: \n";
305                 } else {
306                     $cells[] = '<label>' . get_string('answer', 'lesson') . " {$i}</label>: \n";
307                 }
308                 $cells[] = format_text($answer->answer, $answer->answerformat, $options);
309                 $table->data[] = new html_table_row($cells);
311                 $cells = array();
312                 $cells[] = '<label>' . get_string('matchesanswer', 'lesson') . " {$i}</label>: \n";
313                 $cells[] = format_text($answer->response, $answer->responseformat, $options);
314                 $table->data[] = new html_table_row($cells);
315             }
316             $i++;
317         }
318         return $table;
319     }
320     /**
321      * Updates the page and its answers
322      *
323      * @global moodle_database $DB
324      * @global moodle_page $PAGE
325      * @param stdClass $properties
326      * @return bool
327      */
328     public function update($properties, $context = null, $maxbytes = null) {
329         global $DB, $PAGE;
330         $answers  = $this->get_answers();
331         $properties->id = $this->properties->id;
332         $properties->lessonid = $this->lesson->id;
333         $properties->timemodified = time();
334         $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$PAGE->course->maxbytes), context_module::instance($PAGE->cm->id), 'mod_lesson', 'page_contents', $properties->id);
335         $DB->update_record("lesson_pages", $properties);
337         // Trigger an event: page updated.
338         \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger();
340         // need to add two to offset correct response and wrong response
341         $this->lesson->maxanswers += 2;
342         for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
343             if (!array_key_exists($i, $this->answers)) {
344                 $this->answers[$i] = new stdClass;
345                 $this->answers[$i]->lessonid = $this->lesson->id;
346                 $this->answers[$i]->pageid = $this->id;
347                 $this->answers[$i]->timecreated = $this->timecreated;
348             }
350             if (!empty($properties->answer_editor[$i]) && is_array($properties->answer_editor[$i])) {
351                 $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
352                 $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
353             }
354             if (!empty($properties->response_editor[$i])) {
355                 $this->answers[$i]->response = $properties->response_editor[$i];
356                 $this->answers[$i]->responseformat = 0;
357             }
359             if (isset($properties->jumpto[$i])) {
360                 $this->answers[$i]->jumpto = $properties->jumpto[$i];
361             }
362             if ($this->lesson->custom && isset($properties->score[$i])) {
363                 $this->answers[$i]->score = $properties->score[$i];
364             }
366             // we don't need to check for isset here because properties called it's own isset method.
367             if ($this->answers[$i]->answer != '') {
368                 if (!isset($this->answers[$i]->id)) {
369                     $this->answers[$i]->id =  $DB->insert_record("lesson_answers", $this->answers[$i]);
370                 } else {
371                     $DB->update_record("lesson_answers", $this->answers[$i]->properties());
372                 }
373                 // Save files in answers (no response_editor for matching questions).
374                 $this->save_answers_files($context, $maxbytes, $this->answers[$i], $properties->answer_editor[$i]);
375             } else if ($i < 2) {
376                 if (!isset($this->answers[$i]->id)) {
377                     $this->answers[$i]->id =  $DB->insert_record("lesson_answers", $this->answers[$i]);
378                 } else {
379                     $DB->update_record("lesson_answers", $this->answers[$i]->properties());
380                 }
382                 // Save files in answers (no response_editor for matching questions).
383                 $this->save_answers_files($context, $maxbytes, $this->answers[$i], $properties->answer_editor[$i]);
384             } else if (isset($this->answers[$i]->id)) {
385                 $DB->delete_records('lesson_answers', array('id'=>$this->answers[$i]->id));
386                 unset($this->answers[$i]);
387             }
388         }
389         return true;
390     }
391     public function stats(array &$pagestats, $tries) {
392         $temp = $this->lesson->get_last_attempt($tries);
393         if ($temp->correct) {
394             if (isset($pagestats[$temp->pageid]["correct"])) {
395                 $pagestats[$temp->pageid]["correct"]++;
396             } else {
397                 $pagestats[$temp->pageid]["correct"] = 1;
398             }
399         }
400         if (isset($pagestats[$temp->pageid]["total"])) {
401             $pagestats[$temp->pageid]["total"]++;
402         } else {
403             $pagestats[$temp->pageid]["total"] = 1;
404         }
405         return true;
406     }
407     public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
408         $answers = array();
409         foreach ($this->get_answers() as $answer) {
410             $answers[$answer->id] = $answer;
411         }
412         $formattextdefoptions = new stdClass;
413         $formattextdefoptions->para = false;  //I'll use it widely in this page
414         foreach ($answers as $answer) {
415             if ($n == 0 && $useranswer != null && $useranswer->correct) {
416                 if ($answer->response == null && $useranswer != null) {
417                     $answerdata->response = get_string("thatsthecorrectanswer", "lesson");
418                 } else {
419                     $answerdata->response = $answer->response;
420                 }
421                 if ($this->lesson->custom) {
422                     $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
423                 } else {
424                     $answerdata->score = get_string("receivedcredit", "lesson");
425                 }
426             } elseif ($n == 1 && $useranswer != null && !$useranswer->correct) {
427                 if ($answer->response == null && $useranswer != null) {
428                     $answerdata->response = get_string("thatsthewronganswer", "lesson");
429                 } else {
430                     $answerdata->response = $answer->response;
431                 }
432                 if ($this->lesson->custom) {
433                     $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
434                 } else {
435                     $answerdata->score = get_string("didnotreceivecredit", "lesson");
436                 }
437             } elseif ($n > 1) {
438                 $data = '<label class="accesshide" for="answer_' . $n . '">' . get_string('answer', 'lesson') . '</label>';
439                 $data .= strip_tags(format_string($answer->answer)) . ' ';
440                 if ($useranswer != null) {
441                     $userresponse = explode(",", $useranswer->useranswer);
442                     $data .= '<label class="accesshide" for="stu_answer_response_' . $n . '">' . get_string('matchesanswer', 'lesson') . '</label>';
443                     $data .= "<select class=\"custom-select\" id=\"stu_answer_response_" . $n . "\" " .
444                              "disabled=\"disabled\"><option selected=\"selected\">";
445                     if (array_key_exists($i, $userresponse)) {
446                         $data .= $userresponse[$i];
447                     }
448                     $data .= "</option></select>";
449                 } else {
450                     $data .= '<label class="accesshide" for="answer_response_' . $n . '">' . get_string('matchesanswer', 'lesson') . '</label>';
451                     $data .= "<select class=\"custom-select\" id=\"answer_response_" . $n . "\" " .
452                              "disabled=\"disabled\"><option selected=\"selected\">".strip_tags(format_string($answer->response))."</option></select>";
453                 }
455                 if ($n == 2) {
456                     if (isset($pagestats[$this->properties->id])) {
457                         if (!array_key_exists('correct', $pagestats[$this->properties->id])) {
458                             $pagestats[$this->properties->id]["correct"] = 0;
459                         }
460                         $percent = $pagestats[$this->properties->id]["correct"] / $pagestats[$this->properties->id]["total"] * 100;
461                         $percent = round($percent, 2);
462                         $percent .= "% ".get_string("answeredcorrectly", "lesson");
463                     } else {
464                         $percent = get_string("nooneansweredthisquestion", "lesson");
465                     }
466                 } else {
467                     $percent = '';
468                 }
470                 $answerdata->answers[] = array($data, $percent);
471                 $i++;
472             }
473             $n++;
474             $answerpage->answerdata = $answerdata;
475         }
476         return $answerpage;
477     }
478     public function get_jumps() {
479         global $DB;
480         // The jumps for matching question type are stored in the 1st and 2nd answer record.
481         $jumps = array();
482         if ($answers = $DB->get_records("lesson_answers", array("lessonid" => $this->lesson->id, "pageid" => $this->properties->id), 'id', '*', 0, 2)) {
483             foreach ($answers as $answer) {
484                 $jumps[] = $this->get_jump_name($answer->jumpto);
485             }
486         } else {
487             $jumps[] = $this->get_jump_name($this->properties->nextpageid);
488         }
489         return $jumps;
490     }
493 class lesson_add_page_form_matching extends lesson_add_page_form_base {
495     public $qtype = 'matching';
496     public $qtypestring = 'matching';
497     protected $answerformat = LESSON_ANSWER_HTML;
498     protected $responseformat = '';
500     public function custom_definition() {
502         $this->_form->addElement('header', 'correctresponse', get_string('correctresponse', 'lesson'));
503         $this->_form->addElement('editor', 'answer_editor[0]', get_string('correctresponse', 'lesson'),
504                 array('rows' => '4', 'columns' => '80'),
505                 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
506         $this->_form->setType('answer_editor[0]', PARAM_RAW);
507         $this->_form->setDefault('answer_editor[0]', array('text' => '', 'format' => FORMAT_HTML));
508         $this->add_jumpto(0, get_string('correctanswerjump','lesson'), LESSON_NEXTPAGE);
509         $this->add_score(0, get_string("correctanswerscore", "lesson"), 1);
511         $this->_form->addElement('header', 'wrongresponse', get_string('wrongresponse', 'lesson'));
512         $this->_form->addElement('editor', 'answer_editor[1]', get_string('wrongresponse', 'lesson'),
513                 array('rows' => '4', 'columns' => '80'),
514                 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
515         $this->_form->setType('answer_editor[1]', PARAM_RAW);
516         $this->_form->setDefault('answer_editor[1]', array('text' => '', 'format' => FORMAT_HTML));
518         $this->add_jumpto(1, get_string('wronganswerjump','lesson'), LESSON_THISPAGE);
519         $this->add_score(1, get_string("wronganswerscore", "lesson"), 0);
521         for ($i = 2; $i < $this->_customdata['lesson']->maxanswers+2; $i++) {
522             $this->_form->addElement('header', 'matchingpair'.($i-1), get_string('matchingpair', 'lesson', $i-1));
523             $this->add_answer($i, null, ($i < 4), LESSON_ANSWER_HTML);
524             $required = ($i < 4);
525             $label = get_string('matchesanswer','lesson');
526             $count = $i;
527             $this->_form->addElement('text', 'response_editor['.$count.']', $label, array('size'=>'50'));
528             $this->_form->setType('response_editor['.$count.']', PARAM_NOTAGS);
529             $this->_form->setDefault('response_editor['.$count.']', '');
530             if ($required) {
531                 $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
532             }
533         }
534     }
537 class lesson_display_answer_form_matching extends moodleform {
539     public function definition() {
540         global $USER, $OUTPUT, $PAGE;
541         $mform = $this->_form;
542         $answers = $this->_customdata['answers'];
543         $useranswers = $this->_customdata['useranswers'];
544         $responseoptions = $this->_customdata['responseoptions'];
545         $lessonid = $this->_customdata['lessonid'];
546         $contents = $this->_customdata['contents'];
548         // Disable shortforms.
549         $mform->setDisableShortforms();
551         $mform->addElement('header', 'pageheader');
553         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
555         $hasattempt = false;
556         $disabled = '';
557         if (isset($useranswers) && !empty($useranswers)) {
558             $hasattempt = true;
559             $disabled = array('disabled' => 'disabled');
560         }
562         $options = new stdClass;
563         $options->para = false;
564         $options->noclean = true;
566         $mform->addElement('hidden', 'id');
567         $mform->setType('id', PARAM_INT);
569         $mform->addElement('hidden', 'pageid');
570         $mform->setType('pageid', PARAM_INT);
572         $i = 0;
574         foreach ($answers as $answer) {
575             $mform->addElement('html', '<div class="answeroption">');
576             if ($answer->response != null) {
577                 $responseid = 'response['.$answer->id.']';
578                 if ($hasattempt) {
579                     $responseid = 'response_'.$answer->id;
580                     $mform->addElement('hidden', 'response['.$answer->id.']', htmlspecialchars($useranswers[$i]));
581                     // Temporary fixed until MDL-38885 gets integrated
582                     $mform->setType('response', PARAM_TEXT);
583                 }
584                 $answer = lesson_page_type_matching::rewrite_answers_urls($answer);
585                 $mform->addElement('select', $responseid, format_text($answer->answer,$answer->answerformat,$options), $responseoptions, $disabled);
586                 $mform->setType($responseid, PARAM_TEXT);
587                 if ($hasattempt) {
588                     $mform->setDefault($responseid, htmlspecialchars(trim($useranswers[$i])));
589                 } else {
590                     $mform->setDefault($responseid, 'answeroption');
591                 }
592             }
593             $mform->addElement('html', '</div>');
594             $i++;
595         }
596         if ($hasattempt) {
597             $this->add_action_buttons(null, get_string("nextpage", "lesson"));
598         } else {
599             $this->add_action_buttons(null, get_string("submit", "lesson"));
600         }
601     }