Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Essay question renderer class.
  19   *
  20   * @package    qtype
  21   * @subpackage essay
  22   * @copyright  2009 The Open University
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  
  30  /**
  31   * Generates the output for essay questions.
  32   *
  33   * @copyright  2009 The Open University
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class qtype_essay_renderer extends qtype_renderer {
  37      public function formulation_and_controls(question_attempt $qa,
  38              question_display_options $options) {
  39          global $CFG;
  40          $question = $qa->get_question();
  41  
  42          /** @var qtype_essay_format_renderer_base $responseoutput */
  43          $responseoutput = $question->get_format_renderer($this->page);
  44          $responseoutput->set_displayoptions($options);
  45  
  46          // Answer field.
  47          $step = $qa->get_last_step_with_qt_var('answer');
  48  
  49          if (!$step->has_qt_var('answer') && empty($options->readonly)) {
  50              // Question has never been answered, fill it with response template.
  51              $step = new question_attempt_step(array('answer' => $question->responsetemplate));
  52          }
  53  
  54          if (empty($options->readonly)) {
  55              $answer = $responseoutput->response_area_input('answer', $qa,
  56                      $step, $question->responsefieldlines, $options->context);
  57  
  58          } else {
  59              $answer = $responseoutput->response_area_read_only('answer', $qa,
  60                      $step, $question->responsefieldlines, $options->context);
  61              $answer .= html_writer::nonempty_tag('p', $question->get_word_count_message_for_review($step->get_qt_data()));
  62  
  63              if (!empty($CFG->enableplagiarism)) {
  64                  require_once($CFG->libdir . '/plagiarismlib.php');
  65  
  66                  $answer .= plagiarism_get_links([
  67                      'context' => $options->context->id,
  68                      'component' => $qa->get_question()->qtype->plugin_name(),
  69                      'area' => $qa->get_usage_id(),
  70                      'itemid' => $qa->get_slot(),
  71                      'userid' => $step->get_user_id(),
  72                      'content' => $qa->get_response_summary()]);
  73              }
  74          }
  75  
  76          $files = '';
  77          if ($question->attachments) {
  78              if (empty($options->readonly)) {
  79                  $files = $this->files_input($qa, $question->attachments, $options);
  80  
  81              } else {
  82                  $files = $this->files_read_only($qa, $options);
  83              }
  84          }
  85  
  86          $result = '';
  87          $result .= html_writer::tag('div', $question->format_questiontext($qa),
  88                  array('class' => 'qtext'));
  89  
  90          $result .= html_writer::start_tag('div', array('class' => 'ablock'));
  91          $result .= html_writer::tag('div', $answer, array('class' => 'answer'));
  92  
  93          // If there is a response and min/max word limit is set in the form then check the response word count.
  94          if ($qa->get_state() == question_state::$invalid) {
  95              $result .= html_writer::nonempty_tag('div',
  96                  $question->get_validation_error($step->get_qt_data()), ['class' => 'validationerror']);
  97          }
  98          $result .= html_writer::tag('div', $files, array('class' => 'attachments'));
  99          $result .= html_writer::end_tag('div');
 100  
 101          return $result;
 102      }
 103  
 104      /**
 105       * Displays any attached files when the question is in read-only mode.
 106       * @param question_attempt $qa the question attempt to display.
 107       * @param question_display_options $options controls what should and should
 108       *      not be displayed. Used to get the context.
 109       */
 110      public function files_read_only(question_attempt $qa, question_display_options $options) {
 111          global $CFG;
 112          $files = $qa->get_last_qt_files('attachments', $options->context->id);
 113          $filelist = [];
 114  
 115          $step = $qa->get_last_step_with_qt_var('attachments');
 116  
 117          foreach ($files as $file) {
 118              $out = html_writer::link($qa->get_response_file_url($file),
 119                  $this->output->pix_icon(file_file_icon($file), get_mimetype_description($file),
 120                      'moodle', array('class' => 'icon')) . ' ' . s($file->get_filename()));
 121              if (!empty($CFG->enableplagiarism)) {
 122                  require_once($CFG->libdir . '/plagiarismlib.php');
 123  
 124                  $out .= plagiarism_get_links([
 125                      'context' => $options->context->id,
 126                      'component' => $qa->get_question()->qtype->plugin_name(),
 127                      'area' => $qa->get_usage_id(),
 128                      'itemid' => $qa->get_slot(),
 129                      'userid' => $step->get_user_id(),
 130                      'file' => $file]);
 131              }
 132              $filelist[] = html_writer::tag('li', $out, ['class' => 'mb-2']);
 133          }
 134  
 135          $labelbyid = $qa->get_qt_field_name('attachments') . '_label';
 136  
 137          $fileslabel = $options->add_question_identifier_to_label(get_string('answerfiles', 'qtype_essay'));
 138          $output = html_writer::tag('h4', $fileslabel, ['id' => $labelbyid, 'class' => 'sr-only']);
 139          $output .= html_writer::tag('ul', implode($filelist), [
 140              'aria-labelledby' => $labelbyid,
 141              'class' => 'list-unstyled m-0',
 142          ]);
 143          return $output;
 144      }
 145  
 146      /**
 147       * Displays the input control for when the student should upload a single file.
 148       * @param question_attempt $qa the question attempt to display.
 149       * @param int $numallowed the maximum number of attachments allowed. -1 = unlimited.
 150       * @param question_display_options $options controls what should and should
 151       *      not be displayed. Used to get the context.
 152       */
 153      public function files_input(question_attempt $qa, $numallowed,
 154              question_display_options $options) {
 155          global $CFG, $COURSE;
 156          require_once($CFG->dirroot . '/lib/form/filemanager.php');
 157  
 158          $pickeroptions = new stdClass();
 159          $pickeroptions->mainfile = null;
 160          $pickeroptions->maxfiles = $numallowed;
 161          $pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
 162                  'attachments', $options->context->id);
 163          $pickeroptions->context = $options->context;
 164          $pickeroptions->return_types = FILE_INTERNAL | FILE_CONTROLLED_LINK;
 165  
 166          $pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
 167                  'attachments', $options->context->id);
 168          $pickeroptions->accepted_types = $qa->get_question()->filetypeslist;
 169  
 170          $fm = new form_filemanager($pickeroptions);
 171          $fm->options->maxbytes = get_user_max_upload_file_size(
 172              $this->page->context,
 173              $CFG->maxbytes,
 174              $COURSE->maxbytes,
 175              $qa->get_question()->maxbytes
 176          );
 177          $filesrenderer = $this->page->get_renderer('core', 'files');
 178  
 179          $text = '';
 180          if (!empty($qa->get_question()->filetypeslist)) {
 181              $text = html_writer::tag('p', get_string('acceptedfiletypes', 'qtype_essay'));
 182              $filetypesutil = new \core_form\filetypes_util();
 183              $filetypes = $qa->get_question()->filetypeslist;
 184              $filetypedescriptions = $filetypesutil->describe_file_types($filetypes);
 185              $text .= $this->render_from_template('core_form/filetypes-descriptions', $filetypedescriptions);
 186          }
 187  
 188          $output = html_writer::start_tag('fieldset');
 189          $fileslabel = $options->add_question_identifier_to_label(get_string('answerfiles', 'qtype_essay'));
 190          $output .= html_writer::tag('legend', $fileslabel, ['class' => 'sr-only']);
 191          $output .= $filesrenderer->render($fm);
 192          $output .= html_writer::empty_tag('input', [
 193              'type' => 'hidden',
 194              'name' => $qa->get_qt_field_name('attachments'),
 195              'value' => $pickeroptions->itemid,
 196          ]);
 197          $output .= $text;
 198          $output .= html_writer::end_tag('fieldset');
 199  
 200          return $output;
 201      }
 202  
 203      public function manual_comment(question_attempt $qa, question_display_options $options) {
 204          if ($options->manualcomment != question_display_options::EDITABLE) {
 205              return '';
 206          }
 207  
 208          $question = $qa->get_question();
 209          return html_writer::nonempty_tag('div', $question->format_text(
 210                  $question->graderinfo, $question->graderinfoformat, $qa, 'qtype_essay',
 211                  'graderinfo', $question->id), array('class' => 'graderinfo'));
 212      }
 213  }
 214  
 215  
 216  /**
 217   * A base class to abstract out the differences between different type of
 218   * response format.
 219   *
 220   * @copyright  2011 The Open University
 221   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 222   */
 223  abstract class qtype_essay_format_renderer_base extends plugin_renderer_base {
 224  
 225      /** @var question_display_options Question display options instance for any necessary information for rendering the question. */
 226      protected $displayoptions;
 227  
 228      /**
 229       * Question number setter.
 230       *
 231       * @param question_display_options $displayoptions
 232       */
 233      public function set_displayoptions(question_display_options $displayoptions): void {
 234          $this->displayoptions = $displayoptions;
 235      }
 236  
 237      /**
 238       * Render the students respone when the question is in read-only mode.
 239       * @param string $name the variable name this input edits.
 240       * @param question_attempt $qa the question attempt being display.
 241       * @param question_attempt_step $step the current step.
 242       * @param int $lines approximate size of input box to display.
 243       * @param object $context the context teh output belongs to.
 244       * @return string html to display the response.
 245       */
 246      public abstract function response_area_read_only($name, question_attempt $qa,
 247              question_attempt_step $step, $lines, $context);
 248  
 249      /**
 250       * Render the students respone when the question is in read-only mode.
 251       * @param string $name the variable name this input edits.
 252       * @param question_attempt $qa the question attempt being display.
 253       * @param question_attempt_step $step the current step.
 254       * @param int $lines approximate size of input box to display.
 255       * @param object $context the context teh output belongs to.
 256       * @return string html to display the response for editing.
 257       */
 258      public abstract function response_area_input($name, question_attempt $qa,
 259              question_attempt_step $step, $lines, $context);
 260  
 261      /**
 262       * @return string specific class name to add to the input element.
 263       */
 264      protected abstract function class_name();
 265  }
 266  
 267  /**
 268   * An essay format renderer for essays where the student should not enter
 269   * any inline response.
 270   *
 271   * @copyright  2013 Binghamton University
 272   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 273   */
 274  class qtype_essay_format_noinline_renderer extends qtype_essay_format_renderer_base {
 275  
 276      protected function class_name() {
 277          return 'qtype_essay_noinline';
 278      }
 279  
 280      public function response_area_read_only($name, $qa, $step, $lines, $context) {
 281          return '';
 282      }
 283  
 284      public function response_area_input($name, $qa, $step, $lines, $context) {
 285          return '';
 286      }
 287  
 288  }
 289  
 290  /**
 291   * An essay format renderer for essays where the student should use the HTML
 292   * editor without the file picker.
 293   *
 294   * @copyright  2011 The Open University
 295   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 296   */
 297  class qtype_essay_format_editor_renderer extends qtype_essay_format_renderer_base {
 298      protected function class_name() {
 299          return 'qtype_essay_editor';
 300      }
 301  
 302      public function response_area_read_only($name, $qa, $step, $lines, $context) {
 303          $labelbyid = $qa->get_qt_field_name($name) . '_label';
 304  
 305          $responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
 306          $output = html_writer::tag('h4', $responselabel, ['id' => $labelbyid, 'class' => 'sr-only']);
 307          $output .= html_writer::tag('div', $this->prepare_response($name, $qa, $step, $context), [
 308              'role' => 'textbox',
 309              'aria-readonly' => 'true',
 310              'aria-labelledby' => $labelbyid,
 311              'class' => $this->class_name() . ' qtype_essay_response readonly',
 312              'style' => 'min-height: ' . ($lines * 1.5) . 'em;',
 313          ]);
 314          // Height $lines * 1.5 because that is a typical line-height on web pages.
 315          // That seems to give results that look OK.
 316  
 317          return $output;
 318      }
 319  
 320      public function response_area_input($name, $qa, $step, $lines, $context) {
 321          global $CFG;
 322          require_once($CFG->dirroot . '/repository/lib.php');
 323  
 324          $inputname = $qa->get_qt_field_name($name);
 325          $responseformat = $step->get_qt_var($name . 'format');
 326          $id = $inputname . '_id';
 327  
 328          $editor = editors_get_preferred_editor($responseformat);
 329          $strformats = format_text_menu();
 330          $formats = $editor->get_supported_formats();
 331          foreach ($formats as $fid) {
 332              $formats[$fid] = $strformats[$fid];
 333          }
 334  
 335          list($draftitemid, $response) = $this->prepare_response_for_editing(
 336                  $name, $step, $context);
 337  
 338          $editor->set_text($response);
 339          $editor->use_editor($id, $this->get_editor_options($context),
 340                  $this->get_filepicker_options($context, $draftitemid));
 341  
 342          $responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
 343          $output = html_writer::tag('label', $responselabel, [
 344              'class' => 'sr-only',
 345              'for' => $id,
 346          ]);
 347          $output .= html_writer::start_tag('div', array('class' =>
 348                  $this->class_name() . ' qtype_essay_response'));
 349  
 350          $output .= html_writer::tag('div', html_writer::tag('textarea', s($response),
 351                  array('id' => $id, 'name' => $inputname, 'rows' => $lines, 'cols' => 60, 'class' => 'form-control')));
 352  
 353          $output .= html_writer::start_tag('div');
 354          if (count($formats) == 1) {
 355              reset($formats);
 356              $output .= html_writer::empty_tag('input', array('type' => 'hidden',
 357                      'name' => $inputname . 'format', 'value' => key($formats)));
 358  
 359          } else {
 360              $output .= html_writer::label(get_string('format'), 'menu' . $inputname . 'format', false);
 361              $output .= ' ';
 362              $output .= html_writer::select($formats, $inputname . 'format', $responseformat, '');
 363          }
 364          $output .= html_writer::end_tag('div');
 365  
 366          $output .= $this->filepicker_html($inputname, $draftitemid);
 367  
 368          $output .= html_writer::end_tag('div');
 369          return $output;
 370      }
 371  
 372      /**
 373       * Prepare the response for read-only display.
 374       * @param string $name the variable name this input edits.
 375       * @param question_attempt $qa the question attempt being display.
 376       * @param question_attempt_step $step the current step.
 377       * @param object $context the context the attempt belongs to.
 378       * @return string the response prepared for display.
 379       */
 380      protected function prepare_response($name, question_attempt $qa,
 381              question_attempt_step $step, $context) {
 382          if (!$step->has_qt_var($name)) {
 383              return '';
 384          }
 385  
 386          $formatoptions = new stdClass();
 387          $formatoptions->para = false;
 388          return format_text($step->get_qt_var($name), $step->get_qt_var($name . 'format'),
 389                  $formatoptions);
 390      }
 391  
 392      /**
 393       * Prepare the response for editing.
 394       * @param string $name the variable name this input edits.
 395       * @param question_attempt_step $step the current step.
 396       * @param object $context the context the attempt belongs to.
 397       * @return string the response prepared for display.
 398       */
 399      protected function prepare_response_for_editing($name,
 400              question_attempt_step $step, $context) {
 401          return array(0, $step->get_qt_var($name));
 402      }
 403  
 404      /**
 405       * @param object $context the context the attempt belongs to.
 406       * @return array options for the editor.
 407       */
 408      protected function get_editor_options($context) {
 409          // Disable the text-editor autosave because quiz has it's own auto save function.
 410          return array('context' => $context, 'autosave' => false);
 411      }
 412  
 413      /**
 414       * @param object $context the context the attempt belongs to.
 415       * @param int $draftitemid draft item id.
 416       * @return array filepicker options for the editor.
 417       */
 418      protected function get_filepicker_options($context, $draftitemid) {
 419          return array('return_types'  => FILE_INTERNAL | FILE_EXTERNAL);
 420      }
 421  
 422      /**
 423       * @param string $inputname input field name.
 424       * @param int $draftitemid draft file area itemid.
 425       * @return string HTML for the filepicker, if used.
 426       */
 427      protected function filepicker_html($inputname, $draftitemid) {
 428          return '';
 429      }
 430  }
 431  
 432  
 433  /**
 434   * An essay format renderer for essays where the student should use the HTML
 435   * editor with the file picker.
 436   *
 437   * @copyright  2011 The Open University
 438   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 439   */
 440  class qtype_essay_format_editorfilepicker_renderer extends qtype_essay_format_editor_renderer {
 441      protected function class_name() {
 442          return 'qtype_essay_editorfilepicker';
 443      }
 444  
 445      protected function prepare_response($name, question_attempt $qa,
 446              question_attempt_step $step, $context) {
 447          if (!$step->has_qt_var($name)) {
 448              return '';
 449          }
 450  
 451          $formatoptions = new stdClass();
 452          $formatoptions->para = false;
 453          $text = $qa->rewrite_response_pluginfile_urls($step->get_qt_var($name),
 454                  $context->id, 'answer', $step);
 455          return format_text($text, $step->get_qt_var($name . 'format'), $formatoptions);
 456      }
 457  
 458      protected function prepare_response_for_editing($name,
 459              question_attempt_step $step, $context) {
 460          return $step->prepare_response_files_draft_itemid_with_text(
 461                  $name, $context->id, $step->get_qt_var($name));
 462      }
 463  
 464      /**
 465       * Get editor options for question response text area.
 466       * @param object $context the context the attempt belongs to.
 467       * @return array options for the editor.
 468       */
 469      protected function get_editor_options($context) {
 470          return question_utils::get_editor_options($context);
 471      }
 472  
 473      /**
 474       * Get the options required to configure the filepicker for one of the editor
 475       * toolbar buttons.
 476       * @deprecated since 3.5
 477       * @param mixed $acceptedtypes array of types of '*'.
 478       * @param int $draftitemid the draft area item id.
 479       * @param object $context the context.
 480       * @return object the required options.
 481       */
 482      protected function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
 483          debugging('qtype_essay_format_editorfilepicker_renderer::specific_filepicker_options() is deprecated, ' .
 484              'use question_utils::specific_filepicker_options() instead.', DEBUG_DEVELOPER);
 485  
 486          $filepickeroptions = new stdClass();
 487          $filepickeroptions->accepted_types = $acceptedtypes;
 488          $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
 489          $filepickeroptions->context = $context;
 490          $filepickeroptions->env = 'filepicker';
 491  
 492          $options = initialise_filepicker($filepickeroptions);
 493          $options->context = $context;
 494          $options->client_id = uniqid();
 495          $options->env = 'editor';
 496          $options->itemid = $draftitemid;
 497  
 498          return $options;
 499      }
 500  
 501      /**
 502       * @param object $context the context the attempt belongs to.
 503       * @param int $draftitemid draft item id.
 504       * @return array filepicker options for the editor.
 505       */
 506      protected function get_filepicker_options($context, $draftitemid) {
 507          return question_utils::get_filepicker_options($context, $draftitemid);
 508      }
 509  
 510      protected function filepicker_html($inputname, $draftitemid) {
 511          $nonjspickerurl = new moodle_url('/repository/draftfiles_manager.php', array(
 512              'action' => 'browse',
 513              'env' => 'editor',
 514              'itemid' => $draftitemid,
 515              'subdirs' => false,
 516              'maxfiles' => -1,
 517              'sesskey' => sesskey(),
 518          ));
 519  
 520          return html_writer::empty_tag('input', array('type' => 'hidden',
 521                  'name' => $inputname . ':itemid', 'value' => $draftitemid)) .
 522                  html_writer::tag('noscript', html_writer::tag('div',
 523                      html_writer::tag('object', '', array('type' => 'text/html',
 524                          'data' => $nonjspickerurl, 'height' => 160, 'width' => 600,
 525                          'style' => 'border: 1px solid #000;'))));
 526      }
 527  }
 528  
 529  
 530  /**
 531   * An essay format renderer for essays where the student should use a plain
 532   * input box, but with a normal, proportional font.
 533   *
 534   * @copyright  2011 The Open University
 535   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 536   */
 537  class qtype_essay_format_plain_renderer extends qtype_essay_format_renderer_base {
 538      /**
 539       * @return string the HTML for the textarea.
 540       */
 541      protected function textarea($response, $lines, $attributes) {
 542          $attributes['class'] = $this->class_name() . ' qtype_essay_response form-control';
 543          $attributes['rows'] = $lines;
 544          $attributes['cols'] = 60;
 545          return html_writer::tag('textarea', s($response), $attributes);
 546      }
 547  
 548      protected function class_name() {
 549          return 'qtype_essay_plain';
 550      }
 551  
 552      public function response_area_read_only($name, $qa, $step, $lines, $context) {
 553          $id = $qa->get_qt_field_name($name) . '_id';
 554  
 555          $responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
 556          $output = html_writer::tag('label', $responselabel, ['class' => 'sr-only', 'for' => $id]);
 557          $output .= $this->textarea($step->get_qt_var($name), $lines, ['id' => $id, 'readonly' => 'readonly']);
 558          return $output;
 559      }
 560  
 561      public function response_area_input($name, $qa, $step, $lines, $context) {
 562          $inputname = $qa->get_qt_field_name($name);
 563          $id = $inputname . '_id';
 564  
 565          $responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
 566          $output = html_writer::tag('label', $responselabel, ['class' => 'sr-only', 'for' => $id]);
 567          $output .= $this->textarea($step->get_qt_var($name), $lines, ['name' => $inputname, 'id' => $id]);
 568          $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => $inputname . 'format', 'value' => FORMAT_PLAIN]);
 569  
 570          return $output;
 571      }
 572  }
 573  
 574  
 575  /**
 576   * An essay format renderer for essays where the student should use a plain
 577   * input box with a monospaced font. You might use this, for example, for a
 578   * question where the students should type computer code.
 579   *
 580   * @copyright  2011 The Open University
 581   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 582   */
 583  class qtype_essay_format_monospaced_renderer extends qtype_essay_format_plain_renderer {
 584      protected function class_name() {
 585          return 'qtype_essay_monospaced';
 586      }
 587  }