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