3 // This file is part of Moodle - http://moodle.org/
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.
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/>.
22 * @copyright 2009 Sam Hemelryk
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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() {
41 public function get_typestring() {
42 if ($this->string===null) {
43 $this->string = get_string($this->typeidstring
, 'lesson');
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
));
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.
63 'context' => context_module
::instance($PAGE->cm
->id
),
64 'objectid' => $this->properties
->id
,
66 'pagetype' => $this->get_typestring()
70 $event = \mod_lesson\event\question_viewed
::create($eventparams);
72 return $mform->display();
74 public function check_answer() {
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();
82 $studentanswer = trim($data->answer
);
83 if ($studentanswer === '') {
84 $result->noanswer
= true;
89 $answers = $this->get_answers();
90 foreach ($answers as $answer) {
91 $answer = parent
::rewrite_answers_urls($answer, false);
93 // Applying PARAM_TEXT as it is applied to the answer submitted by the user.
94 $expectedanswer = clean_param($answer->answer
, PARAM_TEXT
);
97 $useregexp = ($this->qoption
);
99 if ($useregexp) { //we are using 'normal analysis', which ignores case
101 if (substr($expectedanswer, -2) == '/i') {
102 $expectedanswer = substr($expectedanswer, 0, -2);
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)) {
117 if (preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer)) {
121 if ($ismatch == true) {
122 $result->correctanswer
= true;
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)) {
130 } else { // we are using regular expressions analysis
131 $startcode = substr($expectedanswer,0,2);
133 //1- check for absence of required string in $studentanswer (coded by initial '--')
135 $expectedanswer = substr($expectedanswer,2);
136 if (!preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer)) {
140 //2- check for code for marking wrong strings (coded by initial '++')
142 $expectedanswer=substr($expectedanswer,2);
144 //check for one or several matches
145 if (preg_match_all('/'.$expectedanswer.'/'.$ignorecase,$studentanswer, $matches)) {
147 $nb = count($matches[0]);
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);
159 //3- check for wrong answers belonging neither to -- nor to ++ categories
161 if (preg_match('/^'.$expectedanswer.'$/'.$ignorecase,$studentanswer, $matches)) {
166 $result->correctanswer
= false;
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);
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;
197 foreach ($answers as $answer) {
198 $answer = parent
::rewrite_answers_urls($answer, false);
200 if ($this->lesson
->custom
&& $answer->score
> 0) {
201 // if the score is > 0, then it is correct
202 $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . ' ' . $i . '</label>';
203 } else if ($this->lesson
->custom
) {
204 $cells[] = '<label>' . get_string('answer', 'lesson') . ' ' . $i . '</label>';
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";
209 $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . ' ' . $i . '</label>';
211 $cells[] = format_text($answer->answer
, $answer->answerformat
, $options);
212 $table->data
[] = new html_table_row($cells);
215 $cells[] = '<label>' . get_string('response', 'lesson') . ' ' . $i . '</label>';
216 $cells[] = format_text($answer->response
, $answer->responseformat
, $options);
217 $table->data
[] = new html_table_row($cells);
220 $cells[] = '<label>' . get_string('score', 'lesson') . '</label>';
221 $cells[] = $answer->score
;
222 $table->data
[] = new html_table_row($cells);
225 $cells[] = '<label>' . get_string('jump', 'lesson') . '</label>';
226 $cells[] = $this->get_jump_name($answer->jumpto
);
227 $table->data
[] = new html_table_row($cells);
229 $table->data
[count($table->data
)-1]->cells
[0]->style
= 'width:20%;';
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];
239 // else, user attempted the question less than the max, so grab the last one
242 if (isset($pagestats[$temp->pageid
][$temp->useranswer
])) {
243 $pagestats[$temp->pageid
][$temp->useranswer
]++
;
245 $pagestats[$temp->pageid
][$temp->useranswer
] = 1;
247 if (isset($pagestats[$temp->pageid
]["total"])) {
248 $pagestats[$temp->pageid
]["total"]++
;
250 $pagestats[$temp->pageid
]["total"] = 1;
255 public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
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);
278 $answerdata->answers
[] = array(get_string("nooneansweredthisquestion", "lesson"), " ");
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");
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");
299 $answerdata->response
= get_string("thatsthewronganswer", "lesson");
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");
309 $answerdata->score
= get_string("didnotreceivecredit", "lesson");
311 // We have found the correct answer, do not process any more answers.
312 $answerpage->answerdata
= $answerdata;
315 $answerdata->response
= get_string("thatsthewronganswer", "lesson");
316 if ($this->lesson
->custom
) {
317 $answerdata->score
= get_string("pointsearned", "lesson").": 0";
319 $answerdata->score
= get_string("didnotreceivecredit", "lesson");
323 $answerpage->answerdata
= $answerdata;
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'];
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';
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
);
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);
394 $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
395 $mform->addElement('text', 'answer', get_string('youranswer', 'lesson'), $attrs);
398 $mform->setType('answer', PARAM_TEXT
);
401 $this->add_action_buttons(null, get_string("nextpage", "lesson"));
403 $this->add_action_buttons(null, get_string("submit", "lesson"));