Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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