MDL-34050 Lesson Module: improved matching question logic for checking user response...
[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
22  * @subpackage lesson
23  * @copyright  2009 Sam Hemelryk
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  **/
27 defined('MOODLE_INTERNAL') || die();
29 /** Matching question type */
30 define("LESSON_PAGE_MATCHING",      "5");
32 class lesson_page_type_matching extends lesson_page {
34     protected $type = lesson_page::TYPE_QUESTION;
35     protected $typeid = LESSON_PAGE_MATCHING;
36     protected $typeidstring = 'matching';
37     protected $string = null;
39     public function get_typeid() {
40         return $this->typeid;
41     }
42     public function get_typestring() {
43         if ($this->string===null) {
44             $this->string = get_string($this->typeidstring, 'lesson');
45         }
46         return $this->string;
47     }
48     public function get_idstring() {
49         return $this->typeidstring;
50     }
51     public function display($renderer, $attempt) {
52         global $USER, $CFG, $PAGE;
53         $mform = $this->make_answer_form($attempt);
54         $data = new stdClass;
55         $data->id = $PAGE->cm->id;
56         $data->pageid = $this->properties->id;
57         $mform->set_data($data);
58         return $mform->display();
59     }
61     protected function make_answer_form($attempt=null) {
62         global $USER, $CFG;
63         // don't shuffle answers (could be an option??)
64         $getanswers = array_slice($this->get_answers(), 2);
66         $answers = array();
67         foreach ($getanswers as $getanswer) {
68             $answers[$getanswer->id] = $getanswer;
69         }
71         $responses = array();
72         foreach ($answers as $answer) {
73             // get all the response
74             if ($answer->response != NULL) {
75                 $responses[$answer->id] = trim($answer->response);
76             }
77         }
79         $responseoptions = array(''=>get_string('choosedots'));
80         if (!empty($responses)) {
81             $shuffleresponses = $responses;
82             shuffle($shuffleresponses);
83             $shuffleresponses = array_unique($shuffleresponses);
84             foreach ($shuffleresponses as  $response) {
85                 $key = array_search($response, $responses);
86                 $responseoptions[$key] = $response;
87             }
88         }
89         if (isset($USER->modattempts[$this->lesson->id]) && !empty($attempt->useranswer)) {
90             $useranswers = explode(',', $attempt->useranswer);
91             $t = 0;
92         } else {
93             $useranswers = array();
94         }
96         $action = $CFG->wwwroot.'/mod/lesson/continue.php';
97         $params = array('answers'=>$answers, 'useranswers'=>$useranswers, 'responseoptions'=>$responseoptions, 'lessonid'=>$this->lesson->id, 'contents'=>$this->get_contents());
98         $mform = new lesson_display_answer_form_matching($action, $params);
99         return $mform;
100     }
102     public function create_answers($properties) {
103         global $DB;
104         // now add the answers
105         $newanswer = new stdClass;
106         $newanswer->lessonid = $this->lesson->id;
107         $newanswer->pageid = $this->properties->id;
108         $newanswer->timecreated = $this->properties->timecreated;
110         $answers = array();
112         // need to add two to offset correct response and wrong response
113         $this->lesson->maxanswers = $this->lesson->maxanswers + 2;
114         for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
115             $answer = clone($newanswer);
116             if (!empty($properties->answer_editor[$i]) && is_array($properties->answer_editor[$i])) {
117                 $answer->answer = $properties->answer_editor[$i]['text'];
118                 $answer->answerformat = $properties->answer_editor[$i]['format'];
119             }
120             if (!empty($properties->response_editor[$i])) {
121                 $answer->response = $properties->response_editor[$i];
122                 $answer->responseformat = 0;
123             }
125             if (isset($properties->jumpto[$i])) {
126                 $answer->jumpto = $properties->jumpto[$i];
127             }
128             if ($this->lesson->custom && isset($properties->score[$i])) {
129                 $answer->score = $properties->score[$i];
130             }
132             if (isset($answer->answer) && $answer->answer != '') {
133                 $answer->id = $DB->insert_record("lesson_answers", $answer);
134                 $answers[$answer->id] = new lesson_page_answer($answer);
135             } else if ($i < 2) {
136                 $answer->id = $DB->insert_record("lesson_answers", $answer);
137                 $answers[$answer->id] = new lesson_page_answer($answer);
138             } else {
139                 break;
140             }
141         }
142         $this->answers = $answers;
143         return $answers;
144     }
146     public function check_answer() {
147         global $CFG, $PAGE;
149         $formattextdefoptions = new stdClass();
150         $formattextdefoptions->noclean = true;
151         $formattextdefoptions->para = false;
153         $result = parent::check_answer();
155         $mform = $this->make_answer_form();
157         $data = $mform->get_data();
158         require_sesskey();
160         if (!$data) {
161             redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$this->properties->id)));
162         }
164         $response = $data->response;
165         if (!is_array($response)) {
166             $result->noanswer = true;
167             return $result;
168         }
170         $answers = $this->get_answers();
172         $correct = array_shift($answers);
173         $wrong   = array_shift($answers);
175         foreach ($answers as $key=>$answer) {
176             if ($answer->answer !== '' or $answer->response !== '') {
177                 $answers[$answer->id] = $answer;
178             }
179             unset($answers[$key]);
180         }
181         // get he users exact responses for record keeping
182         $hits = 0;
183         $userresponse = array();
184         foreach ($response as $id => $value) {
185             $userresponse[] = $value;
186             // Make sure the user's answer is exist in question's answer
187             if (array_key_exists($id, $answers)) {
188                 $answer = $answers[$id];
189                 $result->studentanswer .= '<br />'.format_text($answer->answer, $answer->answerformat, $formattextdefoptions).' = '.$answers[$value]->response;
190                 if ($id == $value) {
191                     $hits++;
192                 }
193             }
194         }
196         $result->userresponse = implode(",", $userresponse);
198         if ($hits == count($answers)) {
199             $result->correctanswer = true;
200             $result->response      = format_text($correct->answer, $correct->answerformat, $formattextdefoptions);
201             $result->answerid      = $correct->id;
202             $result->newpageid     = $correct->jumpto;
203         } else {
204             $result->correctanswer = false;
205             $result->response      = format_text($wrong->answer, $wrong->answerformat, $formattextdefoptions);
206             $result->answerid      = $wrong->id;
207             $result->newpageid     = $wrong->jumpto;
208         }
210         return $result;
211     }
213     public function option_description_string() {
214         return get_string("firstanswershould", "lesson");
215     }
217     public function display_answers(html_table $table) {
218         $answers = $this->get_answers();
219         $options = new stdClass;
220         $options->noclean = true;
221         $options->para = false;
222         $i = 1;
223         $n = 0;
225         foreach ($answers as $answer) {
226             if ($n < 2) {
227                 if ($answer->answer != NULL) {
228                     $cells = array();
229                     if ($n == 0) {
230                         $cells[] = "<span class=\"label\">".get_string("correctresponse", "lesson").'</span>';
231                     } else {
232                         $cells[] = "<span class=\"label\">".get_string("wrongresponse", "lesson").'</span>';
233                     }
234                     $cells[] = format_text($answer->answer, $answer->answerformat, $options);
235                     $table->data[] = new html_table_row($cells);
236                 }
238                 if ($n == 0) {
239                     $cells = array();
240                     $cells[] = '<span class="label">'.get_string("correctanswerscore", "lesson")."</span>: ";
241                     $cells[] = $answer->score;
242                     $table->data[] = new html_table_row($cells);
244                     $cells = array();
245                     $cells[] = '<span class="label">'.get_string("correctanswerjump", "lesson")."</span>: ";
246                     $cells[] = $this->get_jump_name($answer->jumpto);
247                     $table->data[] = new html_table_row($cells);
248                 } elseif ($n == 1) {
249                     $cells = array();
250                     $cells[] = '<span class="label">'.get_string("wronganswerscore", "lesson")."</span>: ";
251                     $cells[] = $answer->score;
252                     $table->data[] = new html_table_row($cells);
254                     $cells = array();
255                     $cells[] = '<span class="label">'.get_string("wronganswerjump", "lesson")."</span>: ";
256                     $cells[] = $this->get_jump_name($answer->jumpto);
257                     $table->data[] = new html_table_row($cells);
258                 }
260                 if ($n === 0){
261                     $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
262                 }
263                 $n++;
264                 $i--;
265             } else {
266                 $cells = array();
267                 if ($this->lesson->custom && $answer->score > 0) {
268                     // if the score is > 0, then it is correct
269                     $cells[] = '<span class="labelcorrect">'.get_string("answer", "lesson")." $i</span>: \n";
270                 } else if ($this->lesson->custom) {
271                     $cells[] = '<span class="label">'.get_string("answer", "lesson")." $i</span>: \n";
272                 } else if ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) {
273                     $cells[] = '<span class="labelcorrect">'.get_string("answer", "lesson")." $i</span>: \n";
274                 } else {
275                     $cells[] = '<span class="label">'.get_string("answer", "lesson")." $i</span>: \n";
276                 }
277                 $cells[] = format_text($answer->answer, $answer->answerformat, $options);
278                 $table->data[] = new html_table_row($cells);
280                 $cells = array();
281                 $cells[] = '<span class="label">'.get_string("matchesanswer", "lesson")." $i</span>: ";
282                 $cells[] = format_text($answer->response, $answer->responseformat, $options);
283                 $table->data[] = new html_table_row($cells);
284             }
285             $i++;
286         }
287         return $table;
288     }
289     /**
290      * Updates the page and its answers
291      *
292      * @global moodle_database $DB
293      * @global moodle_page $PAGE
294      * @param stdClass $properties
295      * @return bool
296      */
297     public function update($properties, $context = null, $maxbytes = null) {
298         global $DB, $PAGE;
299         $answers  = $this->get_answers();
300         $properties->id = $this->properties->id;
301         $properties->lessonid = $this->lesson->id;
302         $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);
303         $DB->update_record("lesson_pages", $properties);
305         // need to add two to offset correct response and wrong response
306         $this->lesson->maxanswers += 2;
307         for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
308             if (!array_key_exists($i, $this->answers)) {
309                 $this->answers[$i] = new stdClass;
310                 $this->answers[$i]->lessonid = $this->lesson->id;
311                 $this->answers[$i]->pageid = $this->id;
312                 $this->answers[$i]->timecreated = $this->timecreated;
313             }
315             if (!empty($properties->answer_editor[$i]) && is_array($properties->answer_editor[$i])) {
316                 $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
317                 $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
318             }
319             if (!empty($properties->response_editor[$i])) {
320                 $this->answers[$i]->response = $properties->response_editor[$i];
321                 $this->answers[$i]->responseformat = 0;
322             }
324             if (isset($properties->jumpto[$i])) {
325                 $this->answers[$i]->jumpto = $properties->jumpto[$i];
326             }
327             if ($this->lesson->custom && isset($properties->score[$i])) {
328                 $this->answers[$i]->score = $properties->score[$i];
329             }
331             // we don't need to check for isset here because properties called it's own isset method.
332             if ($this->answers[$i]->answer != '') {
333                 if (!isset($this->answers[$i]->id)) {
334                     $this->answers[$i]->id =  $DB->insert_record("lesson_answers", $this->answers[$i]);
335                 } else {
336                     $DB->update_record("lesson_answers", $this->answers[$i]->properties());
337                 }
338             } else if ($i < 2) {
339                 if (!isset($this->answers[$i]->id)) {
340                     $this->answers[$i]->id =  $DB->insert_record("lesson_answers", $this->answers[$i]);
341                 } else {
342                     $DB->update_record("lesson_answers", $this->answers[$i]->properties());
343                 }
345             } else if (isset($this->answers[$i]->id)) {
346                 $DB->delete_records('lesson_answers', array('id'=>$this->answers[$i]->id));
347                 unset($this->answers[$i]);
348             }
349         }
350         return true;
351     }
352     public function stats(array &$pagestats, $tries) {
353         if(count($tries) > $this->lesson->maxattempts) { // if there are more tries than the max that is allowed, grab the last "legal" attempt
354             $temp = $tries[$this->lesson->maxattempts - 1];
355         } else {
356             // else, user attempted the question less than the max, so grab the last one
357             $temp = end($tries);
358         }
359         if ($temp->correct) {
360             if (isset($pagestats[$temp->pageid]["correct"])) {
361                 $pagestats[$temp->pageid]["correct"]++;
362             } else {
363                 $pagestats[$temp->pageid]["correct"] = 1;
364             }
365         }
366         if (isset($pagestats[$temp->pageid]["total"])) {
367             $pagestats[$temp->pageid]["total"]++;
368         } else {
369             $pagestats[$temp->pageid]["total"] = 1;
370         }
371         return true;
372     }
373     public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
374         $answers = array();
375         foreach ($this->get_answers() as $answer) {
376             $answers[$answer->id] = $answer;
377         }
378         $formattextdefoptions = new stdClass;
379         $formattextdefoptions->para = false;  //I'll use it widely in this page
380         foreach ($answers as $answer) {
381             if ($n == 0 && $useranswer != NULL && $useranswer->correct) {
382                 if ($answer->response == NULL && $useranswer != NULL) {
383                     $answerdata->response = get_string("thatsthecorrectanswer", "lesson");
384                 } else {
385                     $answerdata->response = $answer->response;
386                 }
387                 if ($this->lesson->custom) {
388                     $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
389                 } else {
390                     $answerdata->score = get_string("receivedcredit", "lesson");
391                 }
392             } elseif ($n == 1 && $useranswer != NULL && !$useranswer->correct) {
393                 if ($answer->response == NULL && $useranswer != NULL) {
394                     $answerdata->response = get_string("thatsthewronganswer", "lesson");
395                 } else {
396                     $answerdata->response = $answer->response;
397                 }
398                 if ($this->lesson->custom) {
399                     $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
400                 } else {
401                     $answerdata->score = get_string("didnotreceivecredit", "lesson");
402                 }
403             } elseif ($n > 1) {
404                 $data = '<label class="accesshide" for="answer_' . $n . '">' . get_string('answer', 'lesson') . '</label>';
405                 $data .= "<select id=\"answer_". $n ."\" disabled=\"disabled\"><option selected=\"selected\">".strip_tags(format_string($answer->answer))."</option></select>";
406                 if ($useranswer != NULL) {
407                     $userresponse = explode(",", $useranswer->useranswer);
408                     $data .= '<label class="accesshide" for="stu_answer_response_' . $n . '">' . get_string('matchesanswer', 'lesson') . '</label>';
409                     $data .= "<select id=\"stu_answer_response_" . $n . "\" disabled=\"disabled\"><option selected=\"selected\">";
410                     if (array_key_exists($i, $userresponse)) {
411                         $data .= strip_tags(format_string($answers[$userresponse[$i]]->response));
412                     }
413                     $data .= "</option></select>";
414                 } else {
415                     $data .= '<label class="accesshide" for="answer_response_' . $n . '">' . get_string('matchesanswer', 'lesson') . '</label>';
416                     $data .= "<select id=\"answer_response_" . $n . "\" disabled=\"disabled\"><option selected=\"selected\">".strip_tags(format_string($answer->response))."</option></select>";
417                 }
419                 if ($n == 2) {
420                     if (isset($pagestats[$this->properties->id])) {
421                         if (!array_key_exists('correct', $pagestats[$this->properties->id])) {
422                             $pagestats[$this->properties->id]["correct"] = 0;
423                         }
424                         $percent = $pagestats[$this->properties->id]["correct"] / $pagestats[$this->properties->id]["total"] * 100;
425                         $percent = round($percent, 2);
426                         $percent .= "% ".get_string("answeredcorrectly", "lesson");
427                     } else {
428                         $percent = get_string("nooneansweredthisquestion", "lesson");
429                     }
430                 } else {
431                     $percent = '';
432                 }
434                 $answerdata->answers[] = array($data, $percent);
435                 $i++;
436             }
437             $n++;
438             $answerpage->answerdata = $answerdata;
439         }
440         return $answerpage;
441     }
442     public function get_jumps() {
443         global $DB;
444         // The jumps for matching question type are stored in the 1st and 2nd answer record.
445         $jumps = array();
446         if ($answers = $DB->get_records("lesson_answers", array("lessonid" => $this->lesson->id, "pageid" => $this->properties->id), 'id', '*', 0, 2)) {
447             foreach ($answers as $answer) {
448                 $jumps[] = $this->get_jump_name($answer->jumpto);
449             }
450         } else {
451             $jumps[] = $this->get_jump_name($this->properties->nextpageid);
452         }
453         return $jumps;
454     }
457 class lesson_add_page_form_matching extends lesson_add_page_form_base {
459     public $qtype = 'matching';
460     public $qtypestring = 'matching';
462     public function custom_definition() {
464         $this->_form->addElement('header', 'correctresponse', get_string('correctresponse', 'lesson'));
465         $this->_form->addElement('editor', 'answer_editor[0]', get_string('correctresponse', 'lesson'), array('rows'=>'4', 'columns'=>'80'), array('noclean'=>true));
466         $this->add_jumpto(0, get_string('correctanswerjump','lesson'), LESSON_NEXTPAGE);
467         $this->add_score(0, get_string("correctanswerscore", "lesson"), 1);
469         $this->_form->addElement('header', 'wrongresponse', get_string('wrongresponse', 'lesson'));
470         $this->_form->addElement('editor', 'answer_editor[1]', get_string('wrongresponse', 'lesson'), array('rows'=>'4', 'columns'=>'80'), array('noclean'=>true));
471         $this->add_jumpto(1, get_string('wronganswerjump','lesson'), LESSON_THISPAGE);
472         $this->add_score(1, get_string("wronganswerscore", "lesson"), 0);
474         for ($i = 2; $i < $this->_customdata['lesson']->maxanswers+2; $i++) {
475             $this->_form->addElement('header', 'matchingpair'.($i-1), get_string('matchingpair', 'lesson', $i-1));
476             $this->add_answer($i, NULL, ($i < 4));
477             $required = ($i < 4);
478             $label = get_string('matchesanswer','lesson');
479             $count = $i;
480             $this->_form->addElement('text', 'response_editor['.$count.']', $label, array('size'=>'50'));
481             $this->_form->setDefault('response_editor['.$count.']', '');
482             if ($required) {
483                 $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
484             }
485         }
486     }
489 class lesson_display_answer_form_matching extends moodleform {
491     public function definition() {
492         global $USER, $OUTPUT;
493         $mform = $this->_form;
494         $answers = $this->_customdata['answers'];
495         $useranswers = $this->_customdata['useranswers'];
496         $responseoptions = $this->_customdata['responseoptions'];
497         $lessonid = $this->_customdata['lessonid'];
498         $contents = $this->_customdata['contents'];
500         $mform->addElement('header', 'pageheader');
502         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
504         $hasattempt = false;
505         $disabled = '';
506         if (isset($useranswers) && !empty($useranswers)) {
507             $hasattempt = true;
508             $disabled = array('disabled' => 'disabled');
509         }
511         $options = new stdClass;
512         $options->para = false;
513         $options->noclean = true;
515         $mform->addElement('hidden', 'id');
516         $mform->setType('id', PARAM_INT);
518         $mform->addElement('hidden', 'pageid');
519         $mform->setType('pageid', PARAM_INT);
521         $i = 0;
522         foreach ($answers as $answer) {
523             $mform->addElement('html', '<div class="answeroption">');
524             if ($answer->response != NULL) {
525                 $responseid = 'response['.$answer->id.']';
526                 if ($hasattempt) {
527                     $responseid = 'response_'.$answer->id;
528                     $mform->addElement('hidden', 'response['.$answer->id.']', htmlspecialchars(trim($answers[$useranswers[$i]]->response)));
529                     $mform->setType('response['.$answer->id.']', PARAM_TEXT);
530                 }
531                 $mform->addElement('select', $responseid, format_text($answer->answer,$answer->answerformat,$options), $responseoptions, $disabled);
532                 $mform->setType($responseid, PARAM_TEXT);
533                 if ($hasattempt) {
534                     $mform->setDefault($responseid, htmlspecialchars(trim($answers[$useranswers[$i]]->response))); //TODO: this is suspicious
535                 } else {
536                     $mform->setDefault($responseid, 'answeroption');
537                 }
538             }
539             $mform->addElement('html', '</div>');
540             $i++;
541         }
542         if ($hasattempt) {
543             $this->add_action_buttons(null, get_string("nextpage", "lesson"));
544         } else {
545             $this->add_action_buttons(null, get_string("submit", "lesson"));
546         }
547     }