MDL-53966 lesson: Allow maximum number of attempts to be unlimited
[moodle.git] / mod / lesson / pagetypes / essay.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  * Essay
20  *
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 /** Essay question type */
29 define("LESSON_PAGE_ESSAY", "10");
31 class lesson_page_type_essay extends lesson_page {
33     protected $type = lesson_page::TYPE_QUESTION;
34     protected $typeidstring = 'essay';
35     protected $typeid = LESSON_PAGE_ESSAY;
36     protected $string = null;
38     public function get_typeid() {
39         return $this->typeid;
40     }
41     public function get_typestring() {
42         if ($this->string===null) {
43             $this->string = get_string($this->typeidstring, 'lesson');
44         }
45         return $this->string;
46     }
47     public function get_idstring() {
48         return $this->typeidstring;
49     }
51     /**
52      * Unserialize attempt useranswer and add missing responseformat if needed
53      * for compatibility with old records.
54      *
55      * @param string $useranswer serialized object
56      * @return object
57      */
58     static public function extract_useranswer($useranswer) {
59         $essayinfo = unserialize($useranswer);
60         if (!isset($essayinfo->responseformat)) {
61             $essayinfo->response = text_to_html($essayinfo->response, false, false);
62             $essayinfo->responseformat = FORMAT_HTML;
63         }
64         return $essayinfo;
65     }
67     public function display($renderer, $attempt) {
68         global $PAGE, $CFG, $USER;
70         $context = context_module::instance($PAGE->cm->id);
71         $options = array(
72             'contents' => $this->get_contents(),
73             'lessonid' => $this->lesson->id,
74             'attemptid' => $attempt ? $attempt->id : null,
75             'editoroptions' => array(
76                 'maxbytes' => $PAGE->course->maxbytes,
77                 'context' => $context,
78                 'noclean' => true,
79                 'maxfiles' => EDITOR_UNLIMITED_FILES,
80                 'enable_filemanagement' => false
81             )
82         );
83         $mform = new lesson_display_answer_form_essay($CFG->wwwroot.'/mod/lesson/continue.php', $options);
85         $data = new stdClass;
86         $data->id = $PAGE->cm->id;
87         $data->pageid = $this->properties->id;
88         if (isset($USER->modattempts[$this->lesson->id])) {
89             $essayinfo = self::extract_useranswer($attempt->useranswer);
90             $data->answer = $essayinfo->answer;
91         }
93         $data = file_prepare_standard_editor($data, 'answer', $options['editoroptions'],
94             $context, 'mod_lesson', 'essay_answers');
95         $mform->set_data($data);
97         // Trigger an event question viewed.
98         $eventparams = array(
99             'context' => context_module::instance($PAGE->cm->id),
100             'objectid' => $this->properties->id,
101             'other' => array(
102                     'pagetype' => $this->get_typestring()
103                 )
104             );
106         $event = \mod_lesson\event\question_viewed::create($eventparams);
107         $event->trigger();
108         return $mform->display();
109     }
110     public function create_answers($properties) {
111         global $DB;
112         // now add the answers
113         $newanswer = new stdClass;
114         $newanswer->lessonid = $this->lesson->id;
115         $newanswer->pageid = $this->properties->id;
116         $newanswer->timecreated = $this->properties->timecreated;
118         if (isset($properties->jumpto[0])) {
119             $newanswer->jumpto = $properties->jumpto[0];
120         }
121         if (isset($properties->score[0])) {
122             $newanswer->score = $properties->score[0];
123         }
124         $newanswer->id = $DB->insert_record("lesson_answers", $newanswer);
125         $answers = array($newanswer->id => new lesson_page_answer($newanswer));
126         $this->answers = $answers;
127         return $answers;
128     }
130     /**
131      * Overridden function
132      *
133      * @param object $attempt
134      * @param object $result
135      * @return array
136      */
137     public function on_after_write_attempt($attempt, $result) {
138         global $PAGE;
140         if ($formdata = $result->postdata) {
141             // Save any linked files if we are using an editor.
142             $editoroptions = array(
143                 'maxbytes' => $PAGE->course->maxbytes,
144                 'context' => context_module::instance($PAGE->cm->id),
145                 'noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES,
146                 'enable_filemanagement' => false,
147             );
149             $formdata = file_postupdate_standard_editor($formdata, 'answer', $editoroptions,
150                 $editoroptions['context'], 'mod_lesson', 'essay_answers', $attempt->id);
152             // Update the student response to have the modified link.
153             $useranswer = unserialize($attempt->useranswer);
154             $useranswer->answer = $formdata->answer;
155             $useranswer->answerformat = $formdata->answerformat;
156             $attempt->useranswer = serialize($useranswer);
158             $result->studentanswer = $formdata->answer;
159             $result->studentanswerformat = $formdata->answerformat;
160             return [$attempt, $result];
161         }
163         return parent::on_after_write_attempt($attempt, $result);
164     }
166     /**
167      * Custom formats the answer to display
168      *
169      * @param string $answer
170      * @param context $context
171      * @param int $answerformat
172      * @param array $options Optional param for additional options.
173      * @return string Returns formatted string
174      */
175     public function format_answer($answer, $context, $answerformat, $options = []) {
176         $answer = file_rewrite_pluginfile_urls($answer, 'pluginfile.php', $context->id,
177             'mod_lesson', 'essay_answers', $options->attemptid);
178         return parent::format_answer($answer, $context, $answerformat, $options);
179     }
181     public function check_answer() {
182         global $PAGE, $CFG;
183         $result = parent::check_answer();
184         $result->isessayquestion = true;
185         $context = context_module::instance($PAGE->cm->id);
186         $options = array(
187             'contents' => $this->get_contents(),
188             'editoroptions' => array(
189                 'maxbytes' => $PAGE->course->maxbytes,
190                 'context' => $context,
191                 'noclean' => true,
192                 'maxfiles' => EDITOR_UNLIMITED_FILES,
193                 'enable_filemanagement' => false,
194             )
195         );
196         $mform = new lesson_display_answer_form_essay($CFG->wwwroot.'/mod/lesson/continue.php', $options);
197         $data = $mform->get_data();
198         require_sesskey();
200         if (!$data) {
201             $result->inmediatejump = true;
202             $result->newpageid = $this->properties->id;
203             return $result;
204         }
206         if (is_array($data->answer_editor) && strlen($data->answer_editor['text'])) {
207             $studentanswer = $data->answer_editor['text']; // Will be reset later.
208             $studentanswerformat = $data->answer_editor['format']; // Will be reset later.
209         } else {
210             $studentanswer = isset($data->answer) ? $data->answer : '';
211             $studentanswerformat = FORMAT_HTML;
212         }
214         if (trim($studentanswer) === '') {
215             $result->noanswer = true;
216             return $result;
217         }
219         $answers = $this->get_answers();
220         foreach ($answers as $answer) {
221             $result->answerid = $answer->id;
222             $result->newpageid = $answer->jumpto;
223         }
225         $userresponse = new stdClass;
226         $userresponse->sent=0;
227         $userresponse->graded = 0;
228         $userresponse->score = 0;
229         $userresponse->answer = $studentanswer;
230         $userresponse->answerformat = $studentanswerformat;
231         $userresponse->response = '';
232         $userresponse->responseformat = FORMAT_HTML;
233         $result->userresponse = serialize($userresponse);
234         $result->studentanswerformat = $studentanswerformat;
235         $result->studentanswer = $studentanswer;
236         $result->postdata = $data;
237         return $result;
238     }
239     public function update($properties, $context = null, $maxbytes = null) {
240         global $DB, $PAGE;
241         $answers  = $this->get_answers();
242         $properties->id = $this->properties->id;
243         $properties->lessonid = $this->lesson->id;
244         $properties->timemodified = time();
245         $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$PAGE->course->maxbytes), context_module::instance($PAGE->cm->id), 'mod_lesson', 'page_contents', $properties->id);
246         $DB->update_record("lesson_pages", $properties);
248         // Trigger an event: page updated.
249         \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger();
251         if (!array_key_exists(0, $this->answers)) {
252             $this->answers[0] = new stdClass;
253             $this->answers[0]->lessonid = $this->lesson->id;
254             $this->answers[0]->pageid = $this->id;
255             $this->answers[0]->timecreated = $this->timecreated;
256         }
257         if (isset($properties->jumpto[0])) {
258             $this->answers[0]->jumpto = $properties->jumpto[0];
259         }
260         if (isset($properties->score[0])) {
261             $this->answers[0]->score = $properties->score[0];
262         }
263         if (!isset($this->answers[0]->id)) {
264             $this->answers[0]->id =  $DB->insert_record("lesson_answers", $this->answers[0]);
265         } else {
266             $DB->update_record("lesson_answers", $this->answers[0]->properties());
267         }
269         return true;
270     }
271     public function stats(array &$pagestats, $tries) {
272         $temp = $this->lesson->get_last_attempt($tries);
273         $essayinfo = self::extract_useranswer($temp->useranswer);
274         if ($essayinfo->graded) {
275             if (isset($pagestats[$temp->pageid])) {
276                 $essaystats = $pagestats[$temp->pageid];
277                 $essaystats->totalscore += $essayinfo->score;
278                 $essaystats->total++;
279                 $pagestats[$temp->pageid] = $essaystats;
280             } else {
281                 $essaystats = new stdClass();
282                 $essaystats->totalscore = $essayinfo->score;
283                 $essaystats->total = 1;
284                 $pagestats[$temp->pageid] = $essaystats;
285             }
286         }
287         return true;
288     }
289     public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
290         global $PAGE, $DB;
292         $formattextdefoptions = new stdClass();
293         $formattextdefoptions->noclean = true;
294         $formattextdefoptions->para = false;
295         $formattextdefoptions->context = $answerpage->context;
296         $answers = $this->get_answers();
297         $context = context_module::instance($PAGE->cm->id);
298         foreach ($answers as $answer) {
299             $hasattempts = $DB->record_exists('lesson_attempts', ['answerid' => $answer->id]);
300             if ($useranswer != null) {
301                 $essayinfo = self::extract_useranswer($useranswer->useranswer);
302                 $essayinfo->answer = file_rewrite_pluginfile_urls($essayinfo->answer, 'pluginfile.php',
303                     $context->id, 'mod_lesson', 'essay_answers', $useranswer->id);
305                 if ($essayinfo->response == null) {
306                     $answerdata->response = get_string("nocommentyet", "lesson");
307                 } else {
308                     $essayinfo->response = file_rewrite_pluginfile_urls($essayinfo->response, 'pluginfile.php',
309                             $answerpage->context->id, 'mod_lesson', 'essay_responses', $useranswer->id);
310                     $answerdata->response  = format_text($essayinfo->response, $essayinfo->responseformat, $formattextdefoptions);
311                 }
312                 if (isset($pagestats[$this->properties->id])) {
313                     $percent = $pagestats[$this->properties->id]->totalscore / $pagestats[$this->properties->id]->total * 100;
314                     $percent = round($percent, 2);
315                     $percent = get_string("averagescore", "lesson").": ". $percent ."%";
316                 } else {
317                     // dont think this should ever be reached....
318                     $percent = get_string("nooneansweredthisquestion", "lesson");
319                 }
320                 if ($essayinfo->graded) {
321                     if ($this->lesson->custom) {
322                         $answerdata->score = get_string("pointsearned", "lesson").": " . $essayinfo->score;
323                     } elseif ($essayinfo->score) {
324                         $answerdata->score = get_string("receivedcredit", "lesson");
325                     } else {
326                         $answerdata->score = get_string("didnotreceivecredit", "lesson");
327                     }
328                 } else {
329                     $answerdata->score = get_string("havenotgradedyet", "lesson");
330                 }
331             } else {
332                 $essayinfo = new stdClass();
333                 if ($hasattempts && has_capability('mod/lesson:grade', $answerpage->context)) {
334                     $essayinfo->answer = html_writer::link(new moodle_url("/mod/lesson/essay.php",
335                         ['id' => $PAGE->cm->id]), get_string("viewessayanswers", "lesson"));
336                 } else {
337                     $essayinfo->answer = "";
338                 }
339                 $essayinfo->answerformat = null;
340             }
342             // The essay question has been graded.
343             if (isset($pagestats[$this->properties->id])) {
344                 $avescore = $pagestats[$this->properties->id]->totalscore / $pagestats[$this->properties->id]->total;
345                 $avescore = round($avescore, 2);
346                 $avescore = get_string("averagescore", "lesson").": ". $avescore ;
347             } else {
348                 $avescore = $hasattempts ? get_string("essaynotgradedyet", "lesson") :
349                         get_string("nooneansweredthisquestion", "lesson");
350             }
351             // This is the student's answer so it should be cleaned.
352             $answerdata->answers[] = array(format_text($essayinfo->answer, $essayinfo->answerformat,
353                     array('para' => true, 'context' => $answerpage->context)), $avescore);
354             $answerpage->answerdata = $answerdata;
355         }
356         return $answerpage;
357     }
358     public function is_unanswered($nretakes) {
359         global $DB, $USER;
360         if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'retry'=>$nretakes))) {
361             return true;
362         }
363         return false;
364     }
365     public function requires_manual_grading() {
366         return true;
367     }
368     public function get_earnedscore($answers, $attempt) {
369         $essayinfo = self::extract_useranswer($attempt->useranswer);
370         return $essayinfo->score;
371     }
374 class lesson_add_page_form_essay extends lesson_add_page_form_base {
376     public $qtype = 'essay';
377     public $qtypestring = 'essay';
379     public function custom_definition() {
381         $this->add_jumpto(0);
382         $this->add_score(0, null, 1);
384     }
387 class lesson_display_answer_form_essay extends moodleform {
389     public function definition() {
390         global $USER, $OUTPUT;
391         $mform = $this->_form;
392         $contents = $this->_customdata['contents'];
393         $editoroptions = $this->_customdata['editoroptions'];
395         $hasattempt = false;
396         $attrs = '';
397         $useranswer = '';
398         $useranswerraw = '';
399         if (isset($this->_customdata['lessonid'])) {
400             $lessonid = $this->_customdata['lessonid'];
401             if (isset($USER->modattempts[$lessonid]->useranswer) && !empty($USER->modattempts[$lessonid]->useranswer)) {
402                 $attrs = array('disabled' => 'disabled');
403                 $hasattempt = true;
404                 $useranswertemp = lesson_page_type_essay::extract_useranswer($USER->modattempts[$lessonid]->useranswer);
405                 $useranswer = htmlspecialchars_decode($useranswertemp->answer, ENT_QUOTES);
406                 $useranswerraw = $useranswertemp->answer;
407             }
408         }
410         // Disable shortforms.
411         $mform->setDisableShortforms();
413         $mform->addElement('header', 'pageheader');
415         $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
417         $options = new stdClass;
418         $options->para = false;
419         $options->noclean = true;
421         $mform->addElement('hidden', 'id');
422         $mform->setType('id', PARAM_INT);
424         $mform->addElement('hidden', 'pageid');
425         $mform->setType('pageid', PARAM_INT);
427         if ($hasattempt) {
428             $mform->addElement('hidden', 'answer', $useranswerraw);
429             $mform->setType('answer', PARAM_RAW);
430             $mform->addElement('html', $OUTPUT->container(get_string('youranswer', 'lesson'), 'youranswer'));
431             $useranswer = file_rewrite_pluginfile_urls($useranswer, 'pluginfile.php', $editoroptions['context']->id,
432                 'mod_lesson', 'essay_answers', $this->_customdata['attemptid']);
433             $mform->addElement('html', $OUTPUT->container($useranswer, 'reviewessay'));
434             $this->add_action_buttons(null, get_string("nextpage", "lesson"));
435         } else {
436             $mform->addElement('editor', 'answer_editor', get_string('youranswer', 'lesson'), null, $editoroptions);
437             $mform->setType('answer_editor', PARAM_RAW);
438             $this->add_action_buttons(null, get_string("submit", "lesson"));
439         }
440     }