MDL-26281 Lesson - Fixed short answer with apostrophe and other html formats
[moodle.git] / mod / lesson / pagetypes / shortanswer.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  * Short answer
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  /** Short answer question type */
30 define("LESSON_PAGE_SHORTANSWER",   "1");
32 class lesson_page_type_shortanswer extends lesson_page {
34     protected $type = lesson_page::TYPE_QUESTION;
35     protected $typeidstring = 'shortanswer';
36     protected $typeid = LESSON_PAGE_SHORTANSWER;
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 = new lesson_display_answer_form_shortanswer($CFG->wwwroot.'/mod/lesson/continue.php', array('contents'=>$this->get_contents()));
54         $data = new stdClass;
55         $data->id = $PAGE->cm->id;
56         $data->pageid = $this->properties->id;
57         if (isset($USER->modattempts[$this->lesson->id])) {
58             $data->answer = s($attempt->useranswer);
59         }
60         $mform->set_data($data);
61         return $mform->display();
62     }
63     public function check_answer() {
64         global $CFG;
65         $result = parent::check_answer();
67         $mform = new lesson_display_answer_form_shortanswer($CFG->wwwroot.'/mod/lesson/continue.php', array('contents'=>$this->get_contents()));
68         $data = $mform->get_data();
69         require_sesskey();
71         $studentanswer = trim($data->answer);
72         if ($studentanswer === '') {
73             $result->noanswer = true;
74             return $result;
75         }
77         $i=0;
78         $answers = $this->get_answers();
79         foreach ($answers as $answer) {
80             $i++;
81             $expectedanswer  = $answer->answer; // for easier handling of $answer->answer
82             $ismatch         = false;
83             $markit          = false;
84             $useregexp       = ($this->qoption);
86             if ($useregexp) { //we are using 'normal analysis', which ignores case
87                 $ignorecase = '';
88                 if (substr($expectedanswer,0,-2) == '/i') {
89                     $expectedanswer = substr($expectedanswer,0,-2);
90                     $ignorecase = 'i';
91                 }
92             } else {
93                 $expectedanswer = str_replace('*', '#####', $expectedanswer);
94                 $expectedanswer = preg_quote($expectedanswer, '/');
95                 $expectedanswer = str_replace('#####', '.*', $expectedanswer);
96             }
97             // see if user typed in any of the correct answers
98             if ((!$this->lesson->custom && $this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) or ($this->lesson->custom && $answer->score > 0) ) {
99                 if (!$useregexp) { // we are using 'normal analysis', which ignores case
100                     if (preg_match('/^'.$expectedanswer.'$/i',$studentanswer)) {
101                         $ismatch = true;
102                     }
103                 } else {
104                     if (preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer)) {
105                         $ismatch = true;
106                     }
107                 }
108                 if ($ismatch == true) {
109                     $result->correctanswer = true;
110                 }
111             } else {
112                if (!$useregexp) { //we are using 'normal analysis'
113                     // see if user typed in any of the wrong answers; don't worry about case
114                     if (preg_match('/^'.$expectedanswer.'$/i',$studentanswer)) {
115                         $ismatch = true;
116                     }
117                 } else { // we are using regular expressions analysis
118                     $startcode = substr($expectedanswer,0,2);
119                     switch ($startcode){
120                         //1- check for absence of required string in $studentanswer (coded by initial '--')
121                         case "--":
122                             $expectedanswer = substr($expectedanswer,2);
123                             if (!preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer)) {
124                                 $ismatch = true;
125                             }
126                             break;
127                         //2- check for code for marking wrong strings (coded by initial '++')
128                         case "++":
129                             $expectedanswer=substr($expectedanswer,2);
130                             $markit = true;
131                             //check for one or several matches
132                             if (preg_match_all('/'.$expectedanswer.'/'.$ignorecase,$studentanswer, $matches)) {
133                                 $ismatch   = true;
134                                 $nb        = count($matches[0]);
135                                 $original  = array();
136                                 $marked    = array();
137                                 $fontStart = '<span class="incorrect matches">';
138                                 $fontEnd   = '</span>';
139                                 for ($i = 0; $i < $nb; $i++) {
140                                     array_push($original,$matches[0][$i]);
141                                     array_push($marked,$fontStart.$matches[0][$i].$fontEnd);
142                                 }
143                                 $studentanswer = str_replace($original, $marked, $studentanswer);
144                             }
145                             break;
146                         //3- check for wrong answers belonging neither to -- nor to ++ categories
147                         default:
148                             if (preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer, $matches)) {
149                                 $ismatch = true;
150                             }
151                             break;
152                     }
153                     $result->correctanswer = false;
154                 }
155             }
156             if ($ismatch) {
157                 $result->newpageid = $answer->jumpto;
158                 if (trim(strip_tags($answer->response))) {
159                     $result->response = $answer->response;
160                 }
161                 $result->answerid = $answer->id;
162                 break; // quit answer analysis immediately after a match has been found
163             }
164         }
165         $result->userresponse = $studentanswer;
166         //clean student answer as it goes to output.
167         $result->studentanswer = s($studentanswer);
168         return $result;
169     }
171     public function option_description_string() {
172         if ($this->properties->qoption) {
173             return " - ".get_string("casesensitive", "lesson");
174         }
175         return parent::option_description_string();
176     }
178     public function display_answers(html_table $table) {
179         $answers = $this->get_answers();
180         $options = new stdClass;
181         $options->noclean = true;
182         $options->para = false;
183         $i = 1;
184         foreach ($answers as $answer) {
185             $cells = array();
186             if ($this->lesson->custom && $answer->score > 0) {
187                 // if the score is > 0, then it is correct
188                 $cells[] = '<span class="labelcorrect">'.get_string("answer", "lesson")." $i</span>: \n";
189             } else if ($this->lesson->custom) {
190                 $cells[] = '<span class="label">'.get_string("answer", "lesson")." $i</span>: \n";
191             } else if ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) {
192                 // underline correct answers
193                 $cells[] = '<span class="correct">'.get_string("answer", "lesson")." $i</span>: \n";
194             } else {
195                 $cells[] = '<span class="labelcorrect">'.get_string("answer", "lesson")." $i</span>: \n";
196             }
197             $cells[] = format_text($answer->answer, $answer->answerformat, $options);
198             $table->data[] = new html_table_row($cells);
200             $cells = array();
201             $cells[] = "<span class=\"label\">".get_string("response", "lesson")." $i</span>";
202             $cells[] = format_text($answer->response, $answer->responseformat, $options);
203             $table->data[] = new html_table_row($cells);
205             $cells = array();
206             $cells[] = "<span class=\"label\">".get_string("score", "lesson").'</span>';
207             $cells[] = $answer->score;
208             $table->data[] = new html_table_row($cells);
210             $cells = array();
211             $cells[] = "<span class=\"label\">".get_string("jump", "lesson").'</span>';
212             $cells[] = $this->get_jump_name($answer->jumpto);
213             $table->data[] = new html_table_row($cells);
214             if ($i === 1){
215                 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
216             }
217             $i++;
218         }
219         return $table;
220     }
221     public function stats(array &$pagestats, $tries) {
222         if(count($tries) > $this->lesson->maxattempts) { // if there are more tries than the max that is allowed, grab the last "legal" attempt
223             $temp = $tries[$this->lesson->maxattempts - 1];
224         } else {
225             // else, user attempted the question less than the max, so grab the last one
226             $temp = end($tries);
227         }
228         if (isset($pagestats[$temp->pageid][$temp->useranswer])) {
229             $pagestats[$temp->pageid][$temp->useranswer]++;
230         } else {
231             $pagestats[$temp->pageid][$temp->useranswer] = 1;
232         }
233         if (isset($pagestats[$temp->pageid]["total"])) {
234             $pagestats[$temp->pageid]["total"]++;
235         } else {
236             $pagestats[$temp->pageid]["total"] = 1;
237         }
238         return true;
239     }
241     public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
242         $answers = $this->get_answers();
243         $formattextdefoptions = new stdClass;
244         $formattextdefoptions->para = false;  //I'll use it widely in this page
245         foreach ($answers as $answer) {
246             if ($useranswer == null && $i == 0) {
247                 // I have the $i == 0 because it is easier to blast through it all at once.
248                 if (isset($pagestats[$this->properties->id])) {
249                     $stats = $pagestats[$this->properties->id];
250                     $total = $stats["total"];
251                     unset($stats["total"]);
252                     foreach ($stats as $valentered => $ntimes) {
253                         $data = '<input type="text" size="50" disabled="disabled" readonly="readonly" value="'.s($valentered).'" />';
254                         $percent = $ntimes / $total * 100;
255                         $percent = round($percent, 2);
256                         $percent .= "% ".get_string("enteredthis", "lesson");
257                         $answerdata->answers[] = array($data, $percent);
258                     }
259                 } else {
260                     $answerdata->answers[] = array(get_string("nooneansweredthisquestion", "lesson"), " ");
261                 }
262                 $i++;
263             } else if ($useranswer != null && ($answer->id == $useranswer->answerid || ($answer == end($answers) && empty($answerdata)))) {
264                  // get in here when what the user entered is not one of the answers
265                 $data = '<input type="text" size="50" disabled="disabled" readonly="readonly" value="'.s($useranswer->useranswer).'">';
266                 if (isset($pagestats[$this->properties->id][$useranswer->useranswer])) {
267                     $percent = $pagestats[$this->properties->id][$useranswer->useranswer] / $pagestats[$this->properties->id]["total"] * 100;
268                     $percent = round($percent, 2);
269                     $percent .= "% ".get_string("enteredthis", "lesson");
270                 } else {
271                     $percent = get_string("nooneenteredthis", "lesson");
272                 }
273                 $answerdata->answers[] = array($data, $percent);
275                 if ($answer->id == $useranswer->answerid) {
276                     if ($answer->response == NULL) {
277                         if ($useranswer->correct) {
278                             $answerdata->response = get_string("thatsthecorrectanswer", "lesson");
279                         } else {
280                             $answerdata->response = get_string("thatsthewronganswer", "lesson");
281                         }
282                     } else {
283                         $answerdata->response = $answer->response;
284                     }
285                     if ($this->lesson->custom) {
286                         $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
287                     } elseif ($useranswer->correct) {
288                         $answerdata->score = get_string("receivedcredit", "lesson");
289                     } else {
290                         $answerdata->score = get_string("didnotreceivecredit", "lesson");
291                     }
292                 } else {
293                     $answerdata->response = get_string("thatsthewronganswer", "lesson");
294                     if ($this->lesson->custom) {
295                         $answerdata->score = get_string("pointsearned", "lesson").": 0";
296                     } else {
297                         $answerdata->score = get_string("didnotreceivecredit", "lesson");
298                     }
299                 }
300             }
301             $answerpage->answerdata = $answerdata;
302         }
303         return $answerpage;
304     }
308 class lesson_add_page_form_shortanswer extends lesson_add_page_form_base {
309     public $qtype = 'shortanswer';
310     public $qtypestring = 'shortanswer';
312     public function custom_definition() {
314         $this->_form->addElement('checkbox', 'qoption', get_string('options', 'lesson'), get_string('casesensitive', 'lesson')); //oh my, this is a regex option!
315         $this->_form->setDefault('qoption', 0);
316         $this->_form->addHelpButton('qoption', 'casesensitive', 'lesson');
318         for ($i = 0; $i < $this->_customdata['lesson']->maxanswers; $i++) {
319             $this->_form->addElement('header', 'answertitle'.$i, get_string('answer').' '.($i+1));
320             $this->add_answer($i);
321             $this->add_response($i);
322             $this->add_jumpto($i, NULL, ($i == 0 ? LESSON_NEXTPAGE : LESSON_THISPAGE));
323             $this->add_score($i, null, ($i===0)?1:0);
324         }
325     }
328 class lesson_display_answer_form_shortanswer extends moodleform {
330     public function definition() {
331         global $OUTPUT;
332         $mform = $this->_form;
333         $contents = $this->_customdata['contents'];
335         $mform->addElement('header', 'pageheader');
337         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
339         $options = new stdClass;
340         $options->para = false;
341         $options->noclean = true;
343         $mform->addElement('hidden', 'id');
344         $mform->setType('id', PARAM_INT);
346         $mform->addElement('hidden', 'pageid');
347         $mform->setType('pageid', PARAM_INT);
349         $mform->addElement('text', 'answer', get_string('youranswer', 'lesson'), array('size'=>'50', 'maxlength'=>'200'));
350         $mform->setType('answer', PARAM_TEXT);
352         $this->add_action_buttons(null, get_string("pleaseenteryouranswerinthebox", "lesson"));
353     }