Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   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   * Renderers for outputting parts of the question engine.
  19   *
  20   * @package    moodlecore
  21   * @subpackage questionengine
  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   * This renderer controls the overall output of questions. It works with a
  32   * {@link qbehaviour_renderer} and a {@link qtype_renderer} to output the
  33   * type-specific bits. The main entry point is the {@link question()} method.
  34   *
  35   * @copyright  2009 The Open University
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class core_question_renderer extends plugin_renderer_base {
  39      public function get_page() {
  40          return $this->page;
  41      }
  42  
  43      /**
  44       * Render an icon, optionally with the word 'Preview' beside it, to preview
  45       * a given question.
  46       * @param int $questionid the id of the question to be previewed.
  47       * @param context $context the context in which the preview is happening.
  48       *      Must be a course or category context.
  49       * @param bool $showlabel if true, show the word 'Preview' after the icon.
  50       *      If false, just show the icon.
  51       */
  52      public function question_preview_link($questionid, context $context, $showlabel) {
  53          if ($showlabel) {
  54              $alt = '';
  55              $label = get_string('preview');
  56              $attributes = array();
  57          } else {
  58              $alt = get_string('preview');
  59              $label = '';
  60              $attributes = array('title' => $alt);
  61          }
  62  
  63          $image = $this->pix_icon('t/preview', $alt, '', array('class' => 'iconsmall'));
  64          $link = question_preview_url($questionid, null, null, null, null, $context);
  65          $action = new popup_action('click', $link, 'questionpreview',
  66                  question_preview_popup_params());
  67  
  68          return $this->action_link($link, $image . $label, $action, $attributes);
  69      }
  70  
  71      /**
  72       * Generate the display of a question in a particular state, and with certain
  73       * display options. Normally you do not call this method directly. Intsead
  74       * you call {@link question_usage_by_activity::render_question()} which will
  75       * call this method with appropriate arguments.
  76       *
  77       * @param question_attempt $qa the question attempt to display.
  78       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
  79       *      specific parts.
  80       * @param qtype_renderer $qtoutput the renderer to output the question type
  81       *      specific parts.
  82       * @param question_display_options $options controls what should and should not be displayed.
  83       * @param string|null $number The question number to display. 'i' is a special
  84       *      value that gets displayed as Information. Null means no number is displayed.
  85       * @return string HTML representation of the question.
  86       */
  87      public function question(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
  88              qtype_renderer $qtoutput, question_display_options $options, $number) {
  89  
  90          $output = '';
  91          $output .= html_writer::start_tag('div', array(
  92              'id' => $qa->get_outer_question_div_unique_id(),
  93              'class' => implode(' ', array(
  94                  'que',
  95                  $qa->get_question(false)->get_type_name(),
  96                  $qa->get_behaviour_name(),
  97                  $qa->get_state_class($options->correctness && $qa->has_marks()),
  98              ))
  99          ));
 100  
 101          $output .= html_writer::tag('div',
 102                  $this->info($qa, $behaviouroutput, $qtoutput, $options, $number),
 103                  array('class' => 'info'));
 104  
 105          $output .= html_writer::start_tag('div', array('class' => 'content'));
 106  
 107          $output .= html_writer::tag('div',
 108                  $this->add_part_heading($qtoutput->formulation_heading(),
 109                      $this->formulation($qa, $behaviouroutput, $qtoutput, $options)),
 110                  array('class' => 'formulation clearfix'));
 111          $output .= html_writer::nonempty_tag('div',
 112                  $this->add_part_heading(get_string('feedback', 'question'),
 113                      $this->outcome($qa, $behaviouroutput, $qtoutput, $options)),
 114                  array('class' => 'outcome clearfix'));
 115          $output .= html_writer::nonempty_tag('div',
 116                  $this->add_part_heading(get_string('comments', 'question'),
 117                      $this->manual_comment($qa, $behaviouroutput, $qtoutput, $options)),
 118                  array('class' => 'comment clearfix'));
 119          $output .= html_writer::nonempty_tag('div',
 120                  $this->response_history($qa, $behaviouroutput, $qtoutput, $options),
 121                  array('class' => 'history clearfix border p-2'));
 122  
 123          $output .= html_writer::end_tag('div');
 124          $output .= html_writer::end_tag('div');
 125          return $output;
 126      }
 127  
 128      /**
 129       * Generate the information bit of the question display that contains the
 130       * metadata like the question number, current state, and mark.
 131       * @param question_attempt $qa the question attempt to display.
 132       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 133       *      specific parts.
 134       * @param qtype_renderer $qtoutput the renderer to output the question type
 135       *      specific parts.
 136       * @param question_display_options $options controls what should and should not be displayed.
 137       * @param string|null $number The question number to display. 'i' is a special
 138       *      value that gets displayed as Information. Null means no number is displayed.
 139       * @return HTML fragment.
 140       */
 141      protected function info(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 142              qtype_renderer $qtoutput, question_display_options $options, $number) {
 143          $output = '';
 144          $output .= $this->number($number);
 145          $output .= $this->status($qa, $behaviouroutput, $options);
 146          $output .= $this->mark_summary($qa, $behaviouroutput, $options);
 147          $output .= $this->question_flag($qa, $options->flags);
 148          $output .= $this->edit_question_link($qa, $options);
 149          return $output;
 150      }
 151  
 152      /**
 153       * Generate the display of the question number.
 154       * @param string|null $number The question number to display. 'i' is a special
 155       *      value that gets displayed as Information. Null means no number is displayed.
 156       * @return HTML fragment.
 157       */
 158      protected function number($number) {
 159          if (trim($number) === '') {
 160              return '';
 161          }
 162          $numbertext = '';
 163          if (trim($number) === 'i') {
 164              $numbertext = get_string('information', 'question');
 165          } else {
 166              $numbertext = get_string('questionx', 'question',
 167                      html_writer::tag('span', $number, array('class' => 'qno')));
 168          }
 169          return html_writer::tag('h3', $numbertext, array('class' => 'no'));
 170      }
 171  
 172      /**
 173       * Add an invisible heading like 'question text', 'feebdack' at the top of
 174       * a section's contents, but only if the section has some content.
 175       * @param string $heading the heading to add.
 176       * @param string $content the content of the section.
 177       * @return string HTML fragment with the heading added.
 178       */
 179      protected function add_part_heading($heading, $content) {
 180          if ($content) {
 181              $content = html_writer::tag('h4', $heading, array('class' => 'accesshide')) . $content;
 182          }
 183          return $content;
 184      }
 185  
 186      /**
 187       * Generate the display of the status line that gives the current state of
 188       * the question.
 189       * @param question_attempt $qa the question attempt to display.
 190       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 191       *      specific parts.
 192       * @param question_display_options $options controls what should and should not be displayed.
 193       * @return HTML fragment.
 194       */
 195      protected function status(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 196              question_display_options $options) {
 197          return html_writer::tag('div', $qa->get_state_string($options->correctness),
 198                  array('class' => 'state'));
 199      }
 200  
 201      /**
 202       * Generate the display of the marks for this question.
 203       * @param question_attempt $qa the question attempt to display.
 204       * @param qbehaviour_renderer $behaviouroutput the behaviour renderer, which can generate a custom display.
 205       * @param question_display_options $options controls what should and should not be displayed.
 206       * @return HTML fragment.
 207       */
 208      protected function mark_summary(question_attempt $qa, qbehaviour_renderer $behaviouroutput, question_display_options $options) {
 209          return html_writer::nonempty_tag('div',
 210                  $behaviouroutput->mark_summary($qa, $this, $options),
 211                  array('class' => 'grade'));
 212      }
 213  
 214      /**
 215       * Generate the display of the marks for this question.
 216       * @param question_attempt $qa the question attempt to display.
 217       * @param question_display_options $options controls what should and should not be displayed.
 218       * @return HTML fragment.
 219       */
 220      public function standard_mark_summary(question_attempt $qa, qbehaviour_renderer $behaviouroutput, question_display_options $options) {
 221          if (!$options->marks) {
 222              return '';
 223  
 224          } else if ($qa->get_max_mark() == 0) {
 225              return get_string('notgraded', 'question');
 226  
 227          } else if ($options->marks == question_display_options::MAX_ONLY ||
 228                  is_null($qa->get_fraction())) {
 229              return $behaviouroutput->marked_out_of_max($qa, $this, $options);
 230  
 231          } else {
 232              return $behaviouroutput->mark_out_of_max($qa, $this, $options);
 233          }
 234      }
 235  
 236      /**
 237       * Generate the display of the available marks for this question.
 238       * @param question_attempt $qa the question attempt to display.
 239       * @param question_display_options $options controls what should and should not be displayed.
 240       * @return HTML fragment.
 241       */
 242      public function standard_marked_out_of_max(question_attempt $qa, question_display_options $options) {
 243          return get_string('markedoutofmax', 'question', $qa->format_max_mark($options->markdp));
 244      }
 245  
 246      /**
 247       * Generate the display of the marks for this question out of the available marks.
 248       * @param question_attempt $qa the question attempt to display.
 249       * @param question_display_options $options controls what should and should not be displayed.
 250       * @return HTML fragment.
 251       */
 252      public function standard_mark_out_of_max(question_attempt $qa, question_display_options $options) {
 253          $a = new stdClass();
 254          $a->mark = $qa->format_mark($options->markdp);
 255          $a->max = $qa->format_max_mark($options->markdp);
 256          return get_string('markoutofmax', 'question', $a);
 257      }
 258  
 259      /**
 260       * Render the question flag, assuming $flagsoption allows it.
 261       *
 262       * @param question_attempt $qa the question attempt to display.
 263       * @param int $flagsoption the option that says whether flags should be displayed.
 264       */
 265      protected function question_flag(question_attempt $qa, $flagsoption) {
 266          global $CFG;
 267  
 268          $divattributes = array('class' => 'questionflag');
 269  
 270          switch ($flagsoption) {
 271              case question_display_options::VISIBLE:
 272                  $flagcontent = $this->get_flag_html($qa->is_flagged());
 273                  break;
 274  
 275              case question_display_options::EDITABLE:
 276                  $id = $qa->get_flag_field_name();
 277                  // The checkbox id must be different from any element name, because
 278                  // of a stupid IE bug:
 279                  // http://www.456bereastreet.com/archive/200802/beware_of_id_and_name_attribute_mixups_when_using_getelementbyid_in_internet_explorer/
 280                  $checkboxattributes = array(
 281                      'type' => 'checkbox',
 282                      'id' => $id . 'checkbox',
 283                      'name' => $id,
 284                      'value' => 1,
 285                  );
 286                  if ($qa->is_flagged()) {
 287                      $checkboxattributes['checked'] = 'checked';
 288                  }
 289                  $postdata = question_flags::get_postdata($qa);
 290  
 291                  $flagcontent = html_writer::empty_tag('input',
 292                                  array('type' => 'hidden', 'name' => $id, 'value' => 0)) .
 293                          html_writer::empty_tag('input', $checkboxattributes) .
 294                          html_writer::empty_tag('input',
 295                                  array('type' => 'hidden', 'value' => $postdata, 'class' => 'questionflagpostdata')) .
 296                          html_writer::tag('label', $this->get_flag_html($qa->is_flagged(), $id . 'img'),
 297                                  array('id' => $id . 'label', 'for' => $id . 'checkbox')) . "\n";
 298  
 299                  $divattributes = array(
 300                      'class' => 'questionflag editable',
 301                      'aria-atomic' => 'true',
 302                      'aria-relevant' => 'text',
 303                      'aria-live' => 'assertive',
 304                  );
 305  
 306                  break;
 307  
 308              default:
 309                  $flagcontent = '';
 310          }
 311  
 312          return html_writer::nonempty_tag('div', $flagcontent, $divattributes);
 313      }
 314  
 315      /**
 316       * Work out the actual img tag needed for the flag
 317       *
 318       * @param bool $flagged whether the question is currently flagged.
 319       * @param string $id an id to be added as an attribute to the img (optional).
 320       * @return string the img tag.
 321       */
 322      protected function get_flag_html($flagged, $id = '') {
 323          if ($flagged) {
 324              $icon = 'i/flagged';
 325              $alt = get_string('flagged', 'question');
 326              $label = get_string('clickunflag', 'question');
 327          } else {
 328              $icon = 'i/unflagged';
 329              $alt = get_string('notflagged', 'question');
 330              $label = get_string('clickflag', 'question');
 331          }
 332          $attributes = array(
 333              'src' => $this->image_url($icon),
 334              'alt' => $alt,
 335              'class' => 'questionflagimage',
 336          );
 337          if ($id) {
 338              $attributes['id'] = $id;
 339          }
 340          $img = html_writer::empty_tag('img', $attributes);
 341          $img .= html_writer::span($label);
 342  
 343          return $img;
 344      }
 345  
 346      protected function edit_question_link(question_attempt $qa,
 347              question_display_options $options) {
 348          global $CFG;
 349  
 350          if (empty($options->editquestionparams)) {
 351              return '';
 352          }
 353  
 354          $params = $options->editquestionparams;
 355          if ($params['returnurl'] instanceof moodle_url) {
 356              $params['returnurl'] = $params['returnurl']->out_as_local_url(false);
 357          }
 358          $params['id'] = $qa->get_question_id();
 359          $editurl = new moodle_url('/question/question.php', $params);
 360  
 361          return html_writer::tag('div', html_writer::link(
 362                  $editurl, $this->pix_icon('t/edit', get_string('edit'), '', array('class' => 'iconsmall')) .
 363                  get_string('editquestion', 'question')),
 364                  array('class' => 'editquestion'));
 365      }
 366  
 367      /**
 368       * Generate the display of the formulation part of the question. This is the
 369       * area that contains the quetsion text, and the controls for students to
 370       * input their answers. Some question types also embed feedback, for
 371       * example ticks and crosses, in this area.
 372       *
 373       * @param question_attempt $qa the question attempt to display.
 374       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 375       *      specific parts.
 376       * @param qtype_renderer $qtoutput the renderer to output the question type
 377       *      specific parts.
 378       * @param question_display_options $options controls what should and should not be displayed.
 379       * @return HTML fragment.
 380       */
 381      protected function formulation(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 382              qtype_renderer $qtoutput, question_display_options $options) {
 383          $output = '';
 384          $output .= html_writer::empty_tag('input', array(
 385                  'type' => 'hidden',
 386                  'name' => $qa->get_control_field_name('sequencecheck'),
 387                  'value' => $qa->get_sequence_check_count()));
 388          $output .= $qtoutput->formulation_and_controls($qa, $options);
 389          if ($options->clearwrong) {
 390              $output .= $qtoutput->clear_wrong($qa);
 391          }
 392          $output .= html_writer::nonempty_tag('div',
 393                  $behaviouroutput->controls($qa, $options), array('class' => 'im-controls'));
 394          return $output;
 395      }
 396  
 397      /**
 398       * Generate the display of the outcome part of the question. This is the
 399       * area that contains the various forms of feedback.
 400       *
 401       * @param question_attempt $qa the question attempt to display.
 402       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 403       *      specific parts.
 404       * @param qtype_renderer $qtoutput the renderer to output the question type
 405       *      specific parts.
 406       * @param question_display_options $options controls what should and should not be displayed.
 407       * @return HTML fragment.
 408       */
 409      protected function outcome(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 410              qtype_renderer $qtoutput, question_display_options $options) {
 411          $output = '';
 412          $output .= html_writer::nonempty_tag('div',
 413                  $qtoutput->feedback($qa, $options), array('class' => 'feedback'));
 414          $output .= html_writer::nonempty_tag('div',
 415                  $behaviouroutput->feedback($qa, $options), array('class' => 'im-feedback'));
 416          $output .= html_writer::nonempty_tag('div',
 417                  $options->extrainfocontent, array('class' => 'extra-feedback'));
 418          return $output;
 419      }
 420  
 421      protected function manual_comment(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 422              qtype_renderer $qtoutput, question_display_options $options) {
 423          return $qtoutput->manual_comment($qa, $options) .
 424                  $behaviouroutput->manual_comment($qa, $options);
 425      }
 426  
 427      /**
 428       * Generate the display of the response history part of the question. This
 429       * is the table showing all the steps the question has been through.
 430       *
 431       * @param question_attempt $qa the question attempt to display.
 432       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 433       *      specific parts.
 434       * @param qtype_renderer $qtoutput the renderer to output the question type
 435       *      specific parts.
 436       * @param question_display_options $options controls what should and should not be displayed.
 437       * @return HTML fragment.
 438       */
 439      protected function response_history(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 440              qtype_renderer $qtoutput, question_display_options $options) {
 441  
 442          if (!$options->history) {
 443              return '';
 444          }
 445  
 446          $table = new html_table();
 447          $table->head  = array (
 448              get_string('step', 'question'),
 449              get_string('time'),
 450              get_string('action', 'question'),
 451              get_string('state', 'question'),
 452          );
 453          if ($options->marks >= question_display_options::MARK_AND_MAX) {
 454              $table->head[] = get_string('marks', 'question');
 455          }
 456  
 457          foreach ($qa->get_full_step_iterator() as $i => $step) {
 458              $stepno = $i + 1;
 459  
 460              $rowclass = '';
 461              if ($stepno == $qa->get_num_steps()) {
 462                  $rowclass = 'current';
 463              } else if (!empty($options->questionreviewlink)) {
 464                  $url = new moodle_url($options->questionreviewlink,
 465                          array('slot' => $qa->get_slot(), 'step' => $i));
 466                  $stepno = $this->output->action_link($url, $stepno,
 467                          new popup_action('click', $url, 'reviewquestion',
 468                                  array('width' => 450, 'height' => 650)),
 469                          array('title' => get_string('reviewresponse', 'question')));
 470              }
 471  
 472              $restrictedqa = new question_attempt_with_restricted_history($qa, $i, null);
 473  
 474              $user = new stdClass();
 475              $user->id = $step->get_user_id();
 476              $row = array(
 477                  $stepno,
 478                  userdate($step->get_timecreated(), get_string('strftimedatetimeshort')),
 479                  s($qa->summarise_action($step)),
 480                  $restrictedqa->get_state_string($options->correctness),
 481              );
 482  
 483              if ($options->marks >= question_display_options::MARK_AND_MAX) {
 484                  $row[] = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp);
 485              }
 486  
 487              $table->rowclasses[] = $rowclass;
 488              $table->data[] = $row;
 489          }
 490  
 491          return html_writer::tag('h4', get_string('responsehistory', 'question'),
 492                          array('class' => 'responsehistoryheader')) .
 493                  $options->extrahistorycontent .
 494                  html_writer::tag('div', html_writer::table($table, true),
 495                          array('class' => 'responsehistoryheader'));
 496      }
 497  
 498  }