Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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   * 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   **/
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /** Essay question type */
  29  define("LESSON_PAGE_ESSAY", "10");
  30  
  31  class lesson_page_type_essay extends lesson_page {
  32  
  33      protected $type = lesson_page::TYPE_QUESTION;
  34      protected $typeidstring = 'essay';
  35      protected $typeid = LESSON_PAGE_ESSAY;
  36      protected $string = null;
  37  
  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      }
  50  
  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_object($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      }
  66  
  67      public function display($renderer, $attempt) {
  68          global $PAGE, $CFG, $USER;
  69  
  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);
  84  
  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          }
  92  
  93          $data = file_prepare_standard_editor($data, 'answer', $options['editoroptions'],
  94              $context, 'mod_lesson', 'essay_answers');
  95          $mform->set_data($data);
  96  
  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              );
 105  
 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;
 117  
 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      }
 129  
 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;
 139  
 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              );
 148  
 149              $formdata = file_postupdate_standard_editor($formdata, 'answer', $editoroptions,
 150                  $editoroptions['context'], 'mod_lesson', 'essay_answers', $attempt->id);
 151  
 152              // Update the student response to have the modified link.
 153              $useranswer = unserialize_object($attempt->useranswer);
 154              $useranswer->answer = $formdata->answer;
 155              $useranswer->answerformat = $formdata->answerformat;
 156              $attempt->useranswer = serialize($useranswer);
 157  
 158              $result->studentanswer = $formdata->answer;
 159              $result->studentanswerformat = $formdata->answerformat;
 160              return [$attempt, $result];
 161          }
 162  
 163          return parent::on_after_write_attempt($attempt, $result);
 164      }
 165  
 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      }
 180  
 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();
 199  
 200          if (!$data) {
 201              $result->inmediatejump = true;
 202              $result->newpageid = $this->properties->id;
 203              return $result;
 204          }
 205  
 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          }
 213  
 214          if (trim($studentanswer) === '') {
 215              $result->noanswer = true;
 216              return $result;
 217          }
 218  
 219          $answers = $this->get_answers();
 220          foreach ($answers as $answer) {
 221              $result->answerid = $answer->id;
 222              $result->newpageid = $answer->jumpto;
 223          }
 224  
 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);
 247  
 248          // Trigger an event: page updated.
 249          \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger();
 250  
 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          }
 268  
 269          return true;
 270      }
 271      public function stats(array &$pagestats, $tries) {
 272          if(count($tries) > $this->lesson->maxattempts) { // if there are more tries than the max that is allowed, grab the last "legal" attempt
 273              $temp = $tries[$this->lesson->maxattempts - 1];
 274          } else {
 275              // else, user attempted the question less than the max, so grab the last one
 276              $temp = end($tries);
 277          }
 278          $essayinfo = self::extract_useranswer($temp->useranswer);
 279          if ($essayinfo->graded) {
 280              if (isset($pagestats[$temp->pageid])) {
 281                  $essaystats = $pagestats[$temp->pageid];
 282                  $essaystats->totalscore += $essayinfo->score;
 283                  $essaystats->total++;
 284                  $pagestats[$temp->pageid] = $essaystats;
 285              } else {
 286                  $essaystats = new stdClass();
 287                  $essaystats->totalscore = $essayinfo->score;
 288                  $essaystats->total = 1;
 289                  $pagestats[$temp->pageid] = $essaystats;
 290              }
 291          }
 292          return true;
 293      }
 294      public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
 295          global $PAGE, $DB;
 296  
 297          $formattextdefoptions = new stdClass();
 298          $formattextdefoptions->noclean = true;
 299          $formattextdefoptions->para = false;
 300          $formattextdefoptions->context = $answerpage->context;
 301          $answers = $this->get_answers();
 302          $context = context_module::instance($PAGE->cm->id);
 303          foreach ($answers as $answer) {
 304              $hasattempts = $DB->record_exists('lesson_attempts', ['answerid' => $answer->id]);
 305              if ($useranswer != null) {
 306                  $essayinfo = self::extract_useranswer($useranswer->useranswer);
 307                  $essayinfo->answer = file_rewrite_pluginfile_urls($essayinfo->answer, 'pluginfile.php',
 308                      $context->id, 'mod_lesson', 'essay_answers', $useranswer->id);
 309  
 310                  if ($essayinfo->response == null) {
 311                      $answerdata->response = get_string("nocommentyet", "lesson");
 312                  } else {
 313                      $essayinfo->response = file_rewrite_pluginfile_urls($essayinfo->response, 'pluginfile.php',
 314                              $answerpage->context->id, 'mod_lesson', 'essay_responses', $useranswer->id);
 315                      $answerdata->response  = format_text($essayinfo->response, $essayinfo->responseformat, $formattextdefoptions);
 316                  }
 317                  if (isset($pagestats[$this->properties->id])) {
 318                      $percent = $pagestats[$this->properties->id]->totalscore / $pagestats[$this->properties->id]->total * 100;
 319                      $percent = round($percent, 2);
 320                      $percent = get_string("averagescore", "lesson").": ". $percent ."%";
 321                  } else {
 322                      // dont think this should ever be reached....
 323                      $percent = get_string("nooneansweredthisquestion", "lesson");
 324                  }
 325                  if ($essayinfo->graded) {
 326                      if ($this->lesson->custom) {
 327                          $answerdata->score = get_string("pointsearned", "lesson").": " . $essayinfo->score;
 328                      } elseif ($essayinfo->score) {
 329                          $answerdata->score = get_string("receivedcredit", "lesson");
 330                      } else {
 331                          $answerdata->score = get_string("didnotreceivecredit", "lesson");
 332                      }
 333                  } else {
 334                      $answerdata->score = get_string("havenotgradedyet", "lesson");
 335                  }
 336              } else {
 337                  $essayinfo = new stdClass();
 338                  if ($hasattempts && has_capability('mod/lesson:grade', $answerpage->context)) {
 339                      $essayinfo->answer = html_writer::link(new moodle_url("/mod/lesson/essay.php",
 340                          ['id' => $PAGE->cm->id]), get_string("viewessayanswers", "lesson"));
 341                  } else {
 342                      $essayinfo->answer = "";
 343                  }
 344                  $essayinfo->answerformat = null;
 345              }
 346  
 347              // The essay question has been graded.
 348              if (isset($pagestats[$this->properties->id])) {
 349                  $avescore = $pagestats[$this->properties->id]->totalscore / $pagestats[$this->properties->id]->total;
 350                  $avescore = round($avescore, 2);
 351                  $avescore = get_string("averagescore", "lesson").": ". $avescore ;
 352              } else {
 353                  $avescore = $hasattempts ? get_string("essaynotgradedyet", "lesson") :
 354                          get_string("nooneansweredthisquestion", "lesson");
 355              }
 356              // This is the student's answer so it should be cleaned.
 357              $answerdata->answers[] = array(format_text($essayinfo->answer, $essayinfo->answerformat,
 358                      array('para' => true, 'context' => $answerpage->context)), $avescore);
 359              $answerpage->answerdata = $answerdata;
 360          }
 361          return $answerpage;
 362      }
 363      public function is_unanswered($nretakes) {
 364          global $DB, $USER;
 365          if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'retry'=>$nretakes))) {
 366              return true;
 367          }
 368          return false;
 369      }
 370      public function requires_manual_grading() {
 371          return true;
 372      }
 373      public function get_earnedscore($answers, $attempt) {
 374          $essayinfo = self::extract_useranswer($attempt->useranswer);
 375          return $essayinfo->score;
 376      }
 377  }
 378  
 379  class lesson_add_page_form_essay extends lesson_add_page_form_base {
 380  
 381      public $qtype = 'essay';
 382      public $qtypestring = 'essay';
 383  
 384      public function custom_definition() {
 385  
 386          $this->add_jumpto(0);
 387          $this->add_score(0, null, 1);
 388  
 389      }
 390  }
 391  
 392  class lesson_display_answer_form_essay extends moodleform {
 393  
 394      public function definition() {
 395          global $USER, $OUTPUT;
 396          $mform = $this->_form;
 397          $contents = $this->_customdata['contents'];
 398          $editoroptions = $this->_customdata['editoroptions'];
 399  
 400          $hasattempt = false;
 401          $attrs = '';
 402          $useranswer = '';
 403          $useranswerraw = '';
 404          if (isset($this->_customdata['lessonid'])) {
 405              $lessonid = $this->_customdata['lessonid'];
 406              if (isset($USER->modattempts[$lessonid]->useranswer) && !empty($USER->modattempts[$lessonid]->useranswer)) {
 407                  $attrs = array('disabled' => 'disabled');
 408                  $hasattempt = true;
 409                  $useranswertemp = lesson_page_type_essay::extract_useranswer($USER->modattempts[$lessonid]->useranswer);
 410                  $useranswer = htmlspecialchars_decode($useranswertemp->answer, ENT_QUOTES);
 411                  $useranswerraw = $useranswertemp->answer;
 412              }
 413          }
 414  
 415          // Disable shortforms.
 416          $mform->setDisableShortforms();
 417  
 418          $mform->addElement('header', 'pageheader');
 419  
 420          $mform->addElement('html', $OUTPUT->container($contents, 'contents'));
 421  
 422          $options = new stdClass;
 423          $options->para = false;
 424          $options->noclean = true;
 425  
 426          $mform->addElement('hidden', 'id');
 427          $mform->setType('id', PARAM_INT);
 428  
 429          $mform->addElement('hidden', 'pageid');
 430          $mform->setType('pageid', PARAM_INT);
 431  
 432          if ($hasattempt) {
 433              $mform->addElement('hidden', 'answer', $useranswerraw);
 434              $mform->setType('answer', PARAM_RAW);
 435              $mform->addElement('html', $OUTPUT->container(get_string('youranswer', 'lesson'), 'youranswer'));
 436              $useranswer = file_rewrite_pluginfile_urls($useranswer, 'pluginfile.php', $editoroptions['context']->id,
 437                  'mod_lesson', 'essay_answers', $this->_customdata['attemptid']);
 438              $mform->addElement('html', $OUTPUT->container($useranswer, 'reviewessay'));
 439              $this->add_action_buttons(null, get_string("nextpage", "lesson"));
 440          } else {
 441              $mform->addElement('editor', 'answer_editor', get_string('youranswer', 'lesson'), null, $editoroptions);
 442              $mform->setType('answer_editor', PARAM_RAW);
 443              $this->add_action_buttons(null, get_string("submit", "lesson"));
 444          }
 445      }
 446  }