weekly release 3.2.1+
[moodle.git] / mod / lesson / pagetypes / shortanswer.php
blobcda674023f56500450c74303f8b74b4afc26d2c9
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.
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
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 /** Short answer question type */
29 define("LESSON_PAGE_SHORTANSWER", "1");
31 class lesson_page_type_shortanswer extends lesson_page {
33 protected $type = lesson_page::TYPE_QUESTION;
34 protected $typeidstring = 'shortanswer';
35 protected $typeid = LESSON_PAGE_SHORTANSWER;
36 protected $string = null;
38 public function get_typeid() {
39 return $this->typeid;
41 public function get_typestring() {
42 if ($this->string===null) {
43 $this->string = get_string($this->typeidstring, 'lesson');
45 return $this->string;
47 public function get_idstring() {
48 return $this->typeidstring;
50 public function display($renderer, $attempt) {
51 global $USER, $CFG, $PAGE;
52 $mform = new lesson_display_answer_form_shortanswer($CFG->wwwroot.'/mod/lesson/continue.php', array('contents'=>$this->get_contents(), 'lessonid'=>$this->lesson->id));
53 $data = new stdClass;
54 $data->id = $PAGE->cm->id;
55 $data->pageid = $this->properties->id;
56 if (isset($USER->modattempts[$this->lesson->id])) {
57 $data->answer = s($attempt->useranswer);
59 $mform->set_data($data);
61 // Trigger an event question viewed.
62 $eventparams = array(
63 'context' => context_module::instance($PAGE->cm->id),
64 'objectid' => $this->properties->id,
65 'other' => array(
66 'pagetype' => $this->get_typestring()
70 $event = \mod_lesson\event\question_viewed::create($eventparams);
71 $event->trigger();
72 return $mform->display();
74 public function check_answer() {
75 global $CFG;
76 $result = parent::check_answer();
78 $mform = new lesson_display_answer_form_shortanswer($CFG->wwwroot.'/mod/lesson/continue.php', array('contents'=>$this->get_contents()));
79 $data = $mform->get_data();
80 require_sesskey();
82 $studentanswer = trim($data->answer);
83 if ($studentanswer === '') {
84 $result->noanswer = true;
85 return $result;
88 $i=0;
89 $answers = $this->get_answers();
90 foreach ($answers as $answer) {
91 $answer = parent::rewrite_answers_urls($answer, false);
92 $i++;
93 // Applying PARAM_TEXT as it is applied to the answer submitted by the user.
94 $expectedanswer = clean_param($answer->answer, PARAM_TEXT);
95 $ismatch = false;
96 $markit = false;
97 $useregexp = ($this->qoption);
99 if ($useregexp) { //we are using 'normal analysis', which ignores case
100 $ignorecase = '';
101 if (substr($expectedanswer, -2) == '/i') {
102 $expectedanswer = substr($expectedanswer, 0, -2);
103 $ignorecase = 'i';
105 } else {
106 $expectedanswer = str_replace('*', '#####', $expectedanswer);
107 $expectedanswer = preg_quote($expectedanswer, '/');
108 $expectedanswer = str_replace('#####', '.*', $expectedanswer);
110 // see if user typed in any of the correct answers
111 if ((!$this->lesson->custom && $this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) or ($this->lesson->custom && $answer->score > 0) ) {
112 if (!$useregexp) { // we are using 'normal analysis', which ignores case
113 if (preg_match('/^'.$expectedanswer.'$/i',$studentanswer)) {
114 $ismatch = true;
116 } else {
117 if (preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer)) {
118 $ismatch = true;
121 if ($ismatch == true) {
122 $result->correctanswer = true;
124 } else {
125 if (!$useregexp) { //we are using 'normal analysis'
126 // see if user typed in any of the wrong answers; don't worry about case
127 if (preg_match('/^'.$expectedanswer.'$/i',$studentanswer)) {
128 $ismatch = true;
130 } else { // we are using regular expressions analysis
131 $startcode = substr($expectedanswer,0,2);
132 switch ($startcode){
133 //1- check for absence of required string in $studentanswer (coded by initial '--')
134 case "--":
135 $expectedanswer = substr($expectedanswer,2);
136 if (!preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer)) {
137 $ismatch = true;
139 break;
140 //2- check for code for marking wrong strings (coded by initial '++')
141 case "++":
142 $expectedanswer=substr($expectedanswer,2);
143 $markit = true;
144 //check for one or several matches
145 if (preg_match_all('/'.$expectedanswer.'/'.$ignorecase,$studentanswer, $matches)) {
146 $ismatch = true;
147 $nb = count($matches[0]);
148 $original = array();
149 $marked = array();
150 $fontStart = '<span class="incorrect matches">';
151 $fontEnd = '</span>';
152 for ($i = 0; $i < $nb; $i++) {
153 array_push($original,$matches[0][$i]);
154 array_push($marked,$fontStart.$matches[0][$i].$fontEnd);
156 $studentanswer = str_replace($original, $marked, $studentanswer);
158 break;
159 //3- check for wrong answers belonging neither to -- nor to ++ categories
160 default:
161 if (preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer, $matches)) {
162 $ismatch = true;
164 break;
166 $result->correctanswer = false;
169 if ($ismatch) {
170 $result->newpageid = $answer->jumpto;
171 $options = new stdClass();
172 $options->para = false;
173 $result->response = format_text($answer->response, $answer->responseformat, $options);
174 $result->answerid = $answer->id;
175 break; // quit answer analysis immediately after a match has been found
178 $result->userresponse = $studentanswer;
179 //clean student answer as it goes to output.
180 $result->studentanswer = s($studentanswer);
181 return $result;
184 public function option_description_string() {
185 if ($this->properties->qoption) {
186 return " - ".get_string("casesensitive", "lesson");
188 return parent::option_description_string();
191 public function display_answers(html_table $table) {
192 $answers = $this->get_answers();
193 $options = new stdClass;
194 $options->noclean = true;
195 $options->para = false;
196 $i = 1;
197 foreach ($answers as $answer) {
198 $answer = parent::rewrite_answers_urls($answer, false);
199 $cells = array();
200 if ($this->lesson->custom && $answer->score > 0) {
201 // if the score is > 0, then it is correct
202 $cells[] = '<span class="labelcorrect">'.get_string("answer", "lesson")." $i</span>: \n";
203 } else if ($this->lesson->custom) {
204 $cells[] = '<span class="label">'.get_string("answer", "lesson")." $i</span>: \n";
205 } else if ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) {
206 // underline correct answers
207 $cells[] = '<span class="correct">'.get_string("answer", "lesson")." $i</span>: \n";
208 } else {
209 $cells[] = '<span class="labelcorrect">'.get_string("answer", "lesson")." $i</span>: \n";
211 $cells[] = format_text($answer->answer, $answer->answerformat, $options);
212 $table->data[] = new html_table_row($cells);
214 $cells = array();
215 $cells[] = "<span class=\"label\">".get_string("response", "lesson")." $i</span>";
216 $cells[] = format_text($answer->response, $answer->responseformat, $options);
217 $table->data[] = new html_table_row($cells);
219 $cells = array();
220 $cells[] = "<span class=\"label\">".get_string("score", "lesson").'</span>';
221 $cells[] = $answer->score;
222 $table->data[] = new html_table_row($cells);
224 $cells = array();
225 $cells[] = "<span class=\"label\">".get_string("jump", "lesson").'</span>';
226 $cells[] = $this->get_jump_name($answer->jumpto);
227 $table->data[] = new html_table_row($cells);
228 if ($i === 1){
229 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
231 $i++;
233 return $table;
235 public function stats(array &$pagestats, $tries) {
236 if(count($tries) > $this->lesson->maxattempts) { // if there are more tries than the max that is allowed, grab the last "legal" attempt
237 $temp = $tries[$this->lesson->maxattempts - 1];
238 } else {
239 // else, user attempted the question less than the max, so grab the last one
240 $temp = end($tries);
242 if (isset($pagestats[$temp->pageid][$temp->useranswer])) {
243 $pagestats[$temp->pageid][$temp->useranswer]++;
244 } else {
245 $pagestats[$temp->pageid][$temp->useranswer] = 1;
247 if (isset($pagestats[$temp->pageid]["total"])) {
248 $pagestats[$temp->pageid]["total"]++;
249 } else {
250 $pagestats[$temp->pageid]["total"] = 1;
252 return true;
255 public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
256 global $PAGE;
258 $answers = $this->get_answers();
259 $formattextdefoptions = new stdClass;
260 $formattextdefoptions->para = false; //I'll use it widely in this page
261 foreach ($answers as $answer) {
262 $answer = parent::rewrite_answers_urls($answer, false);
263 if ($useranswer == null && $i == 0) {
264 // I have the $i == 0 because it is easier to blast through it all at once.
265 if (isset($pagestats[$this->properties->id])) {
266 $stats = $pagestats[$this->properties->id];
267 $total = $stats["total"];
268 unset($stats["total"]);
269 foreach ($stats as $valentered => $ntimes) {
270 $data = '<input type="text" size="50" disabled="disabled" class="form-control" ' .
271 'readonly="readonly" value="'.s($valentered).'" />';
272 $percent = $ntimes / $total * 100;
273 $percent = round($percent, 2);
274 $percent .= "% ".get_string("enteredthis", "lesson");
275 $answerdata->answers[] = array($data, $percent);
277 } else {
278 $answerdata->answers[] = array(get_string("nooneansweredthisquestion", "lesson"), " ");
280 $i++;
281 } else if ($useranswer != null && ($answer->id == $useranswer->answerid || $answer == end($answers))) {
282 // get in here when what the user entered is not one of the answers
283 $data = '<input type="text" size="50" disabled="disabled" class="form-control" ' .
284 'readonly="readonly" value="'.s($useranswer->useranswer).'">';
285 if (isset($pagestats[$this->properties->id][$useranswer->useranswer])) {
286 $percent = $pagestats[$this->properties->id][$useranswer->useranswer] / $pagestats[$this->properties->id]["total"] * 100;
287 $percent = round($percent, 2);
288 $percent .= "% ".get_string("enteredthis", "lesson");
289 } else {
290 $percent = get_string("nooneenteredthis", "lesson");
292 $answerdata->answers[] = array($data, $percent);
294 if ($answer->id == $useranswer->answerid) {
295 if ($answer->response == null) {
296 if ($useranswer->correct) {
297 $answerdata->response = get_string("thatsthecorrectanswer", "lesson");
298 } else {
299 $answerdata->response = get_string("thatsthewronganswer", "lesson");
301 } else {
302 $answerdata->response = $answer->response;
304 if ($this->lesson->custom) {
305 $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
306 } elseif ($useranswer->correct) {
307 $answerdata->score = get_string("receivedcredit", "lesson");
308 } else {
309 $answerdata->score = get_string("didnotreceivecredit", "lesson");
311 // We have found the correct answer, do not process any more answers.
312 $answerpage->answerdata = $answerdata;
313 break;
314 } else {
315 $answerdata->response = get_string("thatsthewronganswer", "lesson");
316 if ($this->lesson->custom) {
317 $answerdata->score = get_string("pointsearned", "lesson").": 0";
318 } else {
319 $answerdata->score = get_string("didnotreceivecredit", "lesson");
323 $answerpage->answerdata = $answerdata;
325 return $answerpage;
330 class lesson_add_page_form_shortanswer extends lesson_add_page_form_base {
331 public $qtype = 'shortanswer';
332 public $qtypestring = 'shortanswer';
333 protected $answerformat = '';
334 protected $responseformat = LESSON_ANSWER_HTML;
336 public function custom_definition() {
338 $this->_form->addElement('checkbox', 'qoption', get_string('options', 'lesson'), get_string('casesensitive', 'lesson')); //oh my, this is a regex option!
339 $this->_form->setDefault('qoption', 0);
340 $this->_form->addHelpButton('qoption', 'casesensitive', 'lesson');
342 for ($i = 0; $i < $this->_customdata['lesson']->maxanswers; $i++) {
343 $this->_form->addElement('header', 'answertitle'.$i, get_string('answer').' '.($i+1));
344 // Only first answer is required.
345 $this->add_answer($i, null, ($i < 1));
346 $this->add_response($i);
347 $this->add_jumpto($i, null, ($i == 0 ? LESSON_NEXTPAGE : LESSON_THISPAGE));
348 $this->add_score($i, null, ($i===0)?1:0);
353 class lesson_display_answer_form_shortanswer extends moodleform {
355 public function definition() {
356 global $OUTPUT, $USER;
357 $mform = $this->_form;
358 $contents = $this->_customdata['contents'];
360 $hasattempt = false;
361 $attrs = array('size'=>'50', 'maxlength'=>'200');
362 if (isset($this->_customdata['lessonid'])) {
363 $lessonid = $this->_customdata['lessonid'];
364 if (isset($USER->modattempts[$lessonid]->useranswer)) {
365 $attrs['readonly'] = 'readonly';
366 $hasattempt = true;
370 $placeholder = false;
371 if (preg_match('/_____+/', $contents, $matches)) {
372 $placeholder = $matches[0];
373 $contentsparts = explode( $placeholder, $contents, 2);
374 $attrs['size'] = round(strlen($placeholder) * 1.1);
377 // Disable shortforms.
378 $mform->setDisableShortforms();
380 $mform->addElement('header', 'pageheader');
381 $mform->addElement('hidden', 'id');
382 $mform->setType('id', PARAM_INT);
384 $mform->addElement('hidden', 'pageid');
385 $mform->setType('pageid', PARAM_INT);
387 if ($placeholder) {
388 $contentsgroup = array();
389 $contentsgroup[] = $mform->createElement('static', '', '', $contentsparts[0]);
390 $contentsgroup[] = $mform->createElement('text', 'answer', '', $attrs);
391 $contentsgroup[] = $mform->createElement('static', '', '', $contentsparts[1]);
392 $mform->addGroup($contentsgroup, '', '', '', false);
393 } else {
394 $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
395 $mform->addElement('text', 'answer', get_string('youranswer', 'lesson'), $attrs);
398 $mform->setType('answer', PARAM_TEXT);
400 if ($hasattempt) {
401 $this->add_action_buttons(null, get_string("nextpage", "lesson"));
402 } else {
403 $this->add_action_buttons(null, get_string("submit", "lesson"));