MDL-53966 lesson: Allow maximum number of attempts to be unlimited
[moodle.git] / mod / lesson / pagetypes / numerical.php
CommitLineData
0a4abb73
SH
1<?php
2
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/>.
17
18/**
19 * Numerical
20 *
9b24f68b 21 * @package mod_lesson
cc3dbaaa
PS
22 * @copyright 2009 Sam Hemelryk
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
0a4abb73
SH
24 **/
25
1e7f8ea2
PS
26defined('MOODLE_INTERNAL') || die();
27
0a4abb73
SH
28/** Numerical question type */
29define("LESSON_PAGE_NUMERICAL", "8");
30
407a6660
P
31use mod_lesson\local\numeric\helper;
32
0a4abb73
SH
33class lesson_page_type_numerical extends lesson_page {
34
35 protected $type = lesson_page::TYPE_QUESTION;
36 protected $typeidstring = 'numerical';
37 protected $typeid = LESSON_PAGE_NUMERICAL;
38 protected $string = null;
39
40 public function get_typeid() {
41 return $this->typeid;
42 }
43 public function get_typestring() {
44 if ($this->string===null) {
45 $this->string = get_string($this->typeidstring, 'lesson');
46 }
47 return $this->string;
48 }
49 public function get_idstring() {
50 return $this->typeidstring;
51 }
52 public function display($renderer, $attempt) {
407a6660
P
53 global $USER, $PAGE;
54 $mform = new lesson_display_answer_form_numerical(new moodle_url('/mod/lesson/continue.php'),
55 array('contents' => $this->get_contents(), 'lessonid' => $this->lesson->id));
0a4abb73
SH
56 $data = new stdClass;
57 $data->id = $PAGE->cm->id;
58 $data->pageid = $this->properties->id;
59 if (isset($USER->modattempts[$this->lesson->id])) {
60 $data->answer = s($attempt->useranswer);
61 }
62 $mform->set_data($data);
8101328a
SB
63
64 // Trigger an event question viewed.
65 $eventparams = array(
66 'context' => context_module::instance($PAGE->cm->id),
67 'objectid' => $this->properties->id,
68 'other' => array(
69 'pagetype' => $this->get_typestring()
70 )
71 );
72
73 $event = \mod_lesson\event\question_viewed::create($eventparams);
74 $event->trigger();
0a4abb73
SH
75 return $mform->display();
76 }
408dd99a
AG
77
78 /**
79 * Creates answers for this page type.
80 *
81 * @param object $properties The answer properties.
82 */
83 public function create_answers($properties) {
84 if (isset($properties->enableotheranswers) && $properties->enableotheranswers) {
85 $properties->response_editor = array_values($properties->response_editor);
86 $properties->jumpto = array_values($properties->jumpto);
87 $properties->score = array_values($properties->score);
88 $wrongresponse = end($properties->response_editor);
89 $wrongkey = key($properties->response_editor);
90 $properties->answer_editor[$wrongkey] = LESSON_OTHER_ANSWERS;
91 }
92 parent::create_answers($properties);
93 }
94
95 /**
96 * Update the answers for this page type.
97 *
98 * @param object $properties The answer properties.
99 * @param context $context The context for this module.
100 * @param int $maxbytes The maximum bytes for any uploades.
101 */
102 public function update($properties, $context = null, $maxbytes = null) {
103 if ($properties->enableotheranswers) {
104 $properties->response_editor = array_values($properties->response_editor);
105 $properties->jumpto = array_values($properties->jumpto);
106 $properties->score = array_values($properties->score);
107 $wrongresponse = end($properties->response_editor);
108 $wrongkey = key($properties->response_editor);
109 $properties->answer_editor[$wrongkey] = LESSON_OTHER_ANSWERS;
110 }
111 parent::update($properties, $context, $maxbytes);
112 }
113
0a4abb73 114 public function check_answer() {
0a4abb73
SH
115 $result = parent::check_answer();
116
407a6660
P
117 $mform = new lesson_display_answer_form_numerical(new moodle_url('/mod/lesson/continue.php'),
118 array('contents' => $this->get_contents()));
0a4abb73
SH
119 $data = $mform->get_data();
120 require_sesskey();
121
9582e530
AG
122 $formattextdefoptions = new stdClass();
123 $formattextdefoptions->noclean = true;
124 $formattextdefoptions->para = false;
125
0a4abb73
SH
126 // set defaults
127 $result->response = '';
128 $result->newpageid = 0;
129
407a6660 130 if (!isset($data->answer)) {
0a4abb73
SH
131 $result->noanswer = true;
132 return $result;
76a83260 133 } else {
407a6660 134 $result->useranswer = $data->answer;
0a4abb73
SH
135 }
136 $result->studentanswer = $result->userresponse = $result->useranswer;
137 $answers = $this->get_answers();
138 foreach ($answers as $answer) {
9582e530 139 $answer = parent::rewrite_answers_urls($answer);
0a4abb73
SH
140 if (strpos($answer->answer, ':')) {
141 // there's a pairs of values
142 list($min, $max) = explode(':', $answer->answer);
143 $minimum = (float) $min;
144 $maximum = (float) $max;
145 } else {
146 // there's only one value
147 $minimum = (float) $answer->answer;
148 $maximum = $minimum;
149 }
150 if (($result->useranswer >= $minimum) && ($result->useranswer <= $maximum)) {
151 $result->newpageid = $answer->jumpto;
9582e530 152 $result->response = format_text($answer->response, $answer->responseformat, $formattextdefoptions);
0a4abb73
SH
153 if ($this->lesson->jumpto_is_correct($this->properties->id, $result->newpageid)) {
154 $result->correctanswer = true;
155 }
156 if ($this->lesson->custom) {
157 if ($answer->score > 0) {
158 $result->correctanswer = true;
159 } else {
160 $result->correctanswer = false;
161 }
162 }
163 $result->answerid = $answer->id;
164 return $result;
165 }
166 }
408dd99a
AG
167 // We could check here to see if we have a wrong answer jump to use.
168 if ($result->answerid == 0) {
169 // Use the all other answers jump details if it is set up.
170 $lastanswer = end($answers);
171 // Double check that this is the @#wronganswer#@ answer.
172 if (strpos($lastanswer->answer, LESSON_OTHER_ANSWERS) !== false) {
173 $otheranswers = end($answers);
174 $result->newpageid = $otheranswers->jumpto;
175 $result->response = format_text($otheranswers->response, $otheranswers->responseformat, $formattextdefoptions);
176 // Does this also need to do the jumpto_is_correct?
177 if ($this->lesson->custom) {
178 $result->correctanswer = ($otheranswers->score > 0);
179 }
180 $result->answerid = $otheranswers->id;
181 }
182 }
0a4abb73
SH
183 return $result;
184 }
185
186 public function display_answers(html_table $table) {
187 $answers = $this->get_answers();
188 $options = new stdClass;
189 $options->noclean = true;
190 $options->para = false;
191 $i = 1;
192 foreach ($answers as $answer) {
0abc18cf 193 $answer = parent::rewrite_answers_urls($answer, false);
0a4abb73
SH
194 $cells = array();
195 if ($this->lesson->custom && $answer->score > 0) {
196 // if the score is > 0, then it is correct
195480e9 197 $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . ' ' . $i . '</label>:';
0a4abb73 198 } else if ($this->lesson->custom) {
195480e9 199 $cells[] = '<label>' . get_string('answer', 'lesson') . ' ' . $i . '</label>:';
0a4abb73
SH
200 } else if ($this->lesson->jumpto_is_correct($this->properties->id, $answer->jumpto)) {
201 // underline correct answers
d6076942 202 $cells[] = '<span class="correct">' . get_string('answer', 'lesson') . ' ' . $i . '</span>:' . "\n";
0a4abb73 203 } else {
195480e9 204 $cells[] = '<label class="correct">' . get_string('answer', 'lesson') . ' ' . $i . '</label>:';
0a4abb73 205 }
407a6660
P
206 $formattedanswer = helper::lesson_format_numeric_value($answer->answer);
207 $cells[] = format_text($formattedanswer, $answer->answerformat, $options);
8cea545e 208 $table->data[] = new html_table_row($cells);
0a4abb73
SH
209
210 $cells = array();
195480e9 211 $cells[] = '<label>' . get_string('response', 'lesson') . ' ' . $i . '</label>:';
01c37ef1 212 $cells[] = format_text($answer->response, $answer->responseformat, $options);
8cea545e 213 $table->data[] = new html_table_row($cells);
0a4abb73
SH
214
215 $cells = array();
195480e9 216 $cells[] = '<label>' . get_string('score', 'lesson') . '</label>:';
0a4abb73 217 $cells[] = $answer->score;
8cea545e 218 $table->data[] = new html_table_row($cells);
0a4abb73
SH
219
220 $cells = array();
195480e9 221 $cells[] = '<label>' . get_string('jump', 'lesson') . '</label>:';
0a4abb73 222 $cells[] = $this->get_jump_name($answer->jumpto);
8cea545e 223 $table->data[] = new html_table_row($cells);
0a4abb73
SH
224 if ($i === 1){
225 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
226 }
227 $i++;
228 }
229 return $table;
230 }
231 public function stats(array &$pagestats, $tries) {
252e85be 232 $temp = $this->lesson->get_last_attempt($tries);
0a4abb73
SH
233 if (isset($pagestats[$temp->pageid][$temp->useranswer])) {
234 $pagestats[$temp->pageid][$temp->useranswer]++;
235 } else {
236 $pagestats[$temp->pageid][$temp->useranswer] = 1;
237 }
238 if (isset($pagestats[$temp->pageid]["total"])) {
239 $pagestats[$temp->pageid]["total"]++;
240 } else {
241 $pagestats[$temp->pageid]["total"] = 1;
242 }
243 return true;
244 }
245
246 public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
247 $answers = $this->get_answers();
248 $formattextdefoptions = new stdClass;
249 $formattextdefoptions->para = false; //I'll use it widely in this page
250 foreach ($answers as $answer) {
251 if ($useranswer == null && $i == 0) {
252 // I have the $i == 0 because it is easier to blast through it all at once.
253 if (isset($pagestats[$this->properties->id])) {
254 $stats = $pagestats[$this->properties->id];
255 $total = $stats["total"];
256 unset($stats["total"]);
257 foreach ($stats as $valentered => $ntimes) {
0f760528 258 $data = '<input class="form-control" type="text" size="50" ' .
407a6660
P
259 'disabled="disabled" readonly="readonly" value="'.
260 s(format_float($valentered, strlen($valentered), true, true)).'" />';
0a4abb73
SH
261 $percent = $ntimes / $total * 100;
262 $percent = round($percent, 2);
263 $percent .= "% ".get_string("enteredthis", "lesson");
264 $answerdata->answers[] = array($data, $percent);
265 }
266 } else {
267 $answerdata->answers[] = array(get_string("nooneansweredthisquestion", "lesson"), " ");
268 }
269 $i++;
91357e7c
JL
270 } else if ($useranswer != null && ($answer->id == $useranswer->answerid || ($answer == end($answers) &&
271 empty($answerdata->answers)))) {
272 // Get in here when the user answered or for the last answer.
0f760528 273 $data = '<input class="form-control" type="text" size="50" ' .
407a6660
P
274 'disabled="disabled" readonly="readonly" value="'.
275 s(format_float($useranswer->useranswer, strlen($useranswer->useranswer), true, true)).'">';
0a4abb73
SH
276 if (isset($pagestats[$this->properties->id][$useranswer->useranswer])) {
277 $percent = $pagestats[$this->properties->id][$useranswer->useranswer] / $pagestats[$this->properties->id]["total"] * 100;
278 $percent = round($percent, 2);
279 $percent .= "% ".get_string("enteredthis", "lesson");
280 } else {
281 $percent = get_string("nooneenteredthis", "lesson");
282 }
283 $answerdata->answers[] = array($data, $percent);
284
285 if ($answer->id == $useranswer->answerid) {
ecea65ca 286 if ($answer->response == null) {
0a4abb73
SH
287 if ($useranswer->correct) {
288 $answerdata->response = get_string("thatsthecorrectanswer", "lesson");
289 } else {
290 $answerdata->response = get_string("thatsthewronganswer", "lesson");
291 }
292 } else {
293 $answerdata->response = $answer->response;
294 }
295 if ($this->lesson->custom) {
296 $answerdata->score = get_string("pointsearned", "lesson").": ".$answer->score;
297 } elseif ($useranswer->correct) {
298 $answerdata->score = get_string("receivedcredit", "lesson");
299 } else {
300 $answerdata->score = get_string("didnotreceivecredit", "lesson");
301 }
302 } else {
303 $answerdata->response = get_string("thatsthewronganswer", "lesson");
304 if ($this->lesson->custom) {
305 $answerdata->score = get_string("pointsearned", "lesson").": 0";
306 } else {
307 $answerdata->score = get_string("didnotreceivecredit", "lesson");
308 }
309 }
310 }
311 $answerpage->answerdata = $answerdata;
312 }
313 return $answerpage;
314 }
408dd99a
AG
315
316 /**
317 * Make updates to the form data if required. In this case to put the all other answer data into the write section of the form.
318 *
319 * @param stdClass $data The form data to update.
320 * @return stdClass The updated fom data.
321 */
322 public function update_form_data(stdClass $data) : stdClass {
323 $answercount = count($this->get_answers());
407a6660
P
324
325 // If no answers provided, then we don't need to check anything.
326 if (!$answercount) {
327 return $data;
328 }
329
408dd99a
AG
330 // Check for other answer entry.
331 $lastanswer = $data->{'answer_editor[' . ($answercount - 1) . ']'};
332 if (strpos($lastanswer, LESSON_OTHER_ANSWERS) !== false) {
333 $data->{'answer_editor[' . ($this->lesson->maxanswers + 1) . ']'} =
334 $data->{'answer_editor[' . ($answercount - 1) . ']'};
335 $data->{'response_editor[' . ($this->lesson->maxanswers + 1) . ']'} =
336 $data->{'response_editor[' . ($answercount - 1) . ']'};
337 $data->{'jumpto[' . ($this->lesson->maxanswers + 1) . ']'} = $data->{'jumpto[' . ($answercount - 1) . ']'};
338 $data->{'score[' . ($this->lesson->maxanswers + 1) . ']'} = $data->{'score[' . ($answercount - 1) . ']'};
339 $data->enableotheranswers = true;
340
341 // Unset the old values.
342 unset($data->{'answer_editor[' . ($answercount - 1) . ']'});
343 unset($data->{'response_editor[' . ($answercount - 1) . ']'});
344 unset($data->{'jumpto['. ($answercount - 1) . ']'});
345 unset($data->{'score[' . ($answercount - 1) . ']'});
346 }
347
348 return $data;
349 }
0a4abb73
SH
350}
351
352class lesson_add_page_form_numerical extends lesson_add_page_form_base {
353
354 public $qtype = 'numerical';
355 public $qtypestring = 'numerical';
ceeab150
RT
356 protected $answerformat = '';
357 protected $responseformat = LESSON_ANSWER_HTML;
0a4abb73
SH
358
359 public function custom_definition() {
408dd99a
AG
360 $answercount = $this->_customdata['lesson']->maxanswers;
361 for ($i = 0; $i < $answercount; $i++) {
0a4abb73 362 $this->_form->addElement('header', 'answertitle'.$i, get_string('answer').' '.($i+1));
407a6660
P
363 $this->add_answer($i, null, ($i < 1), '', [
364 'identifier' => 'numericanswer',
365 'component' => 'mod_lesson'
366 ]);
0a4abb73 367 $this->add_response($i);
ecea65ca 368 $this->add_jumpto($i, null, ($i == 0 ? LESSON_NEXTPAGE : LESSON_THISPAGE));
0a4abb73
SH
369 $this->add_score($i, null, ($i===0)?1:0);
370 }
408dd99a
AG
371 // Wrong answer jump.
372 $this->_form->addElement('header', 'wronganswer', get_string('allotheranswers', 'lesson'));
373 $newcount = $answercount + 1;
374 $this->_form->addElement('advcheckbox', 'enableotheranswers', get_string('enabled', 'lesson'));
375 $this->add_response($newcount);
376 $this->add_jumpto($newcount, get_string('allotheranswersjump', 'lesson'), LESSON_NEXTPAGE);
377 $this->add_score($newcount, get_string('allotheranswersscore', 'lesson'), 0);
0a4abb73 378 }
407a6660
P
379
380 /**
381 * We call get data when storing the data into the db. Override to format the floats properly
382 *
383 * @return object|void
384 */
385 public function get_data() : ?stdClass {
386 $data = parent::get_data();
387
388 if (!empty($data->answer_editor)) {
389 foreach ($data->answer_editor as $key => $answer) {
390 $data->answer_editor[$key] = helper::lesson_unformat_numeric_value($answer);
391 }
392 }
393
394 return $data;
395 }
396
397 /**
398 * Return submitted data if properly submitted or returns NULL if validation fails or
399 * if there is no submitted data with formatted numbers
400 *
401 * @return object submitted data; NULL if not valid or not submitted or cancelled
402 */
403 public function get_submitted_data() : ?stdClass {
404 $data = parent::get_submitted_data();
405
406 if (!empty($data->answer_editor)) {
407 foreach ($data->answer_editor as $key => $answer) {
408 $data->answer_editor[$key] = helper::lesson_unformat_numeric_value($answer);
409 }
410 }
411
412 return $data;
413 }
414
415 /**
416 * Load in existing data as form defaults. Usually new entry defaults are stored directly in
417 * form definition (new entry form); this function is used to load in data where values
418 * already exist and data is being edited (edit entry form) after formatting numbers
419 *
420 *
421 * @param stdClass|array $defaults object or array of default values
422 */
423 public function set_data($defaults) {
424 if (is_object($defaults)) {
425 $defaults = (array) $defaults;
426 }
427
428 $editor = 'answer_editor';
429 foreach ($defaults as $key => $answer) {
430 if (substr($key, 0, strlen($editor)) == $editor) {
431 $defaults[$key] = helper::lesson_format_numeric_value($answer);
432 }
433 }
434
435 parent::set_data($defaults);
436 }
0a4abb73
SH
437}
438
439class lesson_display_answer_form_numerical extends moodleform {
440
441 public function definition() {
442 global $USER, $OUTPUT;
443 $mform = $this->_form;
444 $contents = $this->_customdata['contents'];
445
5b0af6e4
JMV
446 // Disable shortforms.
447 $mform->setDisableShortforms();
448
ffdf7f8a
DM
449 $mform->addElement('header', 'pageheader');
450
451 $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
0a4abb73 452
54fd7cd9
RW
453 $hasattempt = false;
454 $attrs = array('size'=>'50', 'maxlength'=>'200');
455 if (isset($this->_customdata['lessonid'])) {
456 $lessonid = $this->_customdata['lessonid'];
457 if (isset($USER->modattempts[$lessonid]->useranswer)) {
458 $attrs['readonly'] = 'readonly';
459 $hasattempt = true;
460 }
461 }
0a4abb73
SH
462 $options = new stdClass;
463 $options->para = false;
464 $options->noclean = true;
465
466 $mform->addElement('hidden', 'id');
467 $mform->setType('id', PARAM_INT);
468
469 $mform->addElement('hidden', 'pageid');
470 $mform->setType('pageid', PARAM_INT);
471
407a6660 472 $mform->addElement('float', 'answer', get_string('youranswer', 'lesson'), $attrs);
0a4abb73 473
54fd7cd9
RW
474 if ($hasattempt) {
475 $this->add_action_buttons(null, get_string("nextpage", "lesson"));
476 } else {
477 $this->add_action_buttons(null, get_string("submit", "lesson"));
478 }
0a4abb73 479 }
ffdf7f8a 480}