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   * 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          $divattributes = array('class' => 'questionflag');
 267  
 268          switch ($flagsoption) {
 269              case question_display_options::VISIBLE:
 270                  $flagcontent = $this->get_flag_html($qa->is_flagged());
 271                  break;
 272  
 273              case question_display_options::EDITABLE:
 274                  $id = $qa->get_flag_field_name();
 275                  // The checkbox id must be different from any element name, because
 276                  // of a stupid IE bug:
 277                  // http://www.456bereastreet.com/archive/200802/beware_of_id_and_name_attribute_mixups_when_using_getelementbyid_in_internet_explorer/
 278                  $checkboxattributes = array(
 279                      'type' => 'checkbox',
 280                      'id' => $id . 'checkbox',
 281                      'name' => $id,
 282                      'value' => 1,
 283                  );
 284                  if ($qa->is_flagged()) {
 285                      $checkboxattributes['checked'] = 'checked';
 286                  }
 287                  $postdata = question_flags::get_postdata($qa);
 288  
 289                  $flagcontent = html_writer::empty_tag('input',
 290                                  array('type' => 'hidden', 'name' => $id, 'value' => 0)) .
 291                          html_writer::empty_tag('input',
 292                                  array('type' => 'hidden', 'value' => $postdata, 'class' => 'questionflagpostdata')) .
 293                          html_writer::empty_tag('input', $checkboxattributes) .
 294                          html_writer::tag('label', $this->get_flag_html($qa->is_flagged(), $id . 'img'),
 295                                  array('id' => $id . 'label', 'for' => $id . 'checkbox')) . "\n";
 296  
 297                  $divattributes = array(
 298                      'class' => 'questionflag editable',
 299                  );
 300  
 301                  break;
 302  
 303              default:
 304                  $flagcontent = '';
 305          }
 306  
 307          return html_writer::nonempty_tag('div', $flagcontent, $divattributes);
 308      }
 309  
 310      /**
 311       * Work out the actual img tag needed for the flag
 312       *
 313       * @param bool $flagged whether the question is currently flagged.
 314       * @param string $id an id to be added as an attribute to the img (optional).
 315       * @return string the img tag.
 316       */
 317      protected function get_flag_html($flagged, $id = '') {
 318          if ($flagged) {
 319              $icon = 'i/flagged';
 320              $label = get_string('clickunflag', 'question');
 321          } else {
 322              $icon = 'i/unflagged';
 323              $label = get_string('clickflag', 'question');
 324          }
 325          $attributes = [
 326              'src' => $this->image_url($icon),
 327              'alt' => '',
 328              'class' => 'questionflagimage',
 329          ];
 330          if ($id) {
 331              $attributes['id'] = $id;
 332          }
 333          $img = html_writer::empty_tag('img', $attributes);
 334          $img .= html_writer::span($label);
 335  
 336          return $img;
 337      }
 338  
 339      /**
 340       * Generate the display of the edit question link.
 341       *
 342       * @param question_attempt $qa The question attempt to display.
 343       * @param question_display_options $options controls what should and should not be displayed.
 344       * @return string
 345       */
 346      protected function edit_question_link(question_attempt $qa, question_display_options $options) {
 347          if (empty($options->editquestionparams)) {
 348              return '';
 349          }
 350  
 351          $params = $options->editquestionparams;
 352          if ($params['returnurl'] instanceof moodle_url) {
 353              $params['returnurl'] = $params['returnurl']->out_as_local_url(false);
 354          }
 355          $params['id'] = $qa->get_question_id();
 356          $editurl = new moodle_url('/question/question.php', $params);
 357  
 358          return html_writer::tag('div', html_writer::link(
 359                  $editurl, $this->pix_icon('t/edit', get_string('edit'), '', array('class' => 'iconsmall')) .
 360                  get_string('editquestion', 'question')),
 361                  array('class' => 'editquestion'));
 362      }
 363  
 364      /**
 365       * Generate the display of the formulation part of the question. This is the
 366       * area that contains the quetsion text, and the controls for students to
 367       * input their answers. Some question types also embed feedback, for
 368       * example ticks and crosses, in this area.
 369       *
 370       * @param question_attempt $qa the question attempt to display.
 371       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 372       *      specific parts.
 373       * @param qtype_renderer $qtoutput the renderer to output the question type
 374       *      specific parts.
 375       * @param question_display_options $options controls what should and should not be displayed.
 376       * @return HTML fragment.
 377       */
 378      protected function formulation(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 379              qtype_renderer $qtoutput, question_display_options $options) {
 380          $output = '';
 381          $output .= html_writer::empty_tag('input', array(
 382                  'type' => 'hidden',
 383                  'name' => $qa->get_control_field_name('sequencecheck'),
 384                  'value' => $qa->get_sequence_check_count()));
 385          $output .= $qtoutput->formulation_and_controls($qa, $options);
 386          if ($options->clearwrong) {
 387              $output .= $qtoutput->clear_wrong($qa);
 388          }
 389          $output .= html_writer::nonempty_tag('div',
 390                  $behaviouroutput->controls($qa, $options), array('class' => 'im-controls'));
 391          return $output;
 392      }
 393  
 394      /**
 395       * Generate the display of the outcome part of the question. This is the
 396       * area that contains the various forms of feedback.
 397       *
 398       * @param question_attempt $qa the question attempt to display.
 399       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 400       *      specific parts.
 401       * @param qtype_renderer $qtoutput the renderer to output the question type
 402       *      specific parts.
 403       * @param question_display_options $options controls what should and should not be displayed.
 404       * @return HTML fragment.
 405       */
 406      protected function outcome(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 407              qtype_renderer $qtoutput, question_display_options $options) {
 408          $output = '';
 409          $output .= html_writer::nonempty_tag('div',
 410                  $qtoutput->feedback($qa, $options), array('class' => 'feedback'));
 411          $output .= html_writer::nonempty_tag('div',
 412                  $behaviouroutput->feedback($qa, $options), array('class' => 'im-feedback'));
 413          $output .= html_writer::nonempty_tag('div',
 414                  $options->extrainfocontent, array('class' => 'extra-feedback'));
 415          return $output;
 416      }
 417  
 418      protected function manual_comment(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 419              qtype_renderer $qtoutput, question_display_options $options) {
 420          return $qtoutput->manual_comment($qa, $options) .
 421                  $behaviouroutput->manual_comment($qa, $options);
 422      }
 423  
 424      /**
 425       * Generate the display of the response history part of the question. This
 426       * is the table showing all the steps the question has been through.
 427       *
 428       * @param question_attempt $qa the question attempt to display.
 429       * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
 430       *      specific parts.
 431       * @param qtype_renderer $qtoutput the renderer to output the question type
 432       *      specific parts.
 433       * @param question_display_options $options controls what should and should not be displayed.
 434       * @return HTML fragment.
 435       */
 436      protected function response_history(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
 437              qtype_renderer $qtoutput, question_display_options $options) {
 438  
 439          if (!$options->history) {
 440              return '';
 441          }
 442  
 443          $table = new html_table();
 444          $table->head  = array (
 445              get_string('step', 'question'),
 446              get_string('time'),
 447              get_string('action', 'question'),
 448              get_string('state', 'question'),
 449          );
 450          if ($options->marks >= question_display_options::MARK_AND_MAX) {
 451              $table->head[] = get_string('marks', 'question');
 452          }
 453  
 454          foreach ($qa->get_full_step_iterator() as $i => $step) {
 455              $stepno = $i + 1;
 456  
 457              $rowclass = '';
 458              if ($stepno == $qa->get_num_steps()) {
 459                  $rowclass = 'current';
 460              } else if (!empty($options->questionreviewlink)) {
 461                  $url = new moodle_url($options->questionreviewlink,
 462                          array('slot' => $qa->get_slot(), 'step' => $i));
 463                  $stepno = $this->output->action_link($url, $stepno,
 464                          new popup_action('click', $url, 'reviewquestion',
 465                                  array('width' => 450, 'height' => 650)),
 466                          array('title' => get_string('reviewresponse', 'question')));
 467              }
 468  
 469              $restrictedqa = new question_attempt_with_restricted_history($qa, $i, null);
 470  
 471              $row = [$stepno,
 472                      userdate($step->get_timecreated(), get_string('strftimedatetimeshort')),
 473                      s($qa->summarise_action($step)) . $this->action_author($step, $options),
 474                      $restrictedqa->get_state_string($options->correctness)];
 475  
 476              if ($options->marks >= question_display_options::MARK_AND_MAX) {
 477                  $row[] = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp);
 478              }
 479  
 480              $table->rowclasses[] = $rowclass;
 481              $table->data[] = $row;
 482          }
 483  
 484          return html_writer::tag('h4', get_string('responsehistory', 'question'),
 485                          array('class' => 'responsehistoryheader')) .
 486                  $options->extrahistorycontent .
 487                  html_writer::tag('div', html_writer::table($table, true),
 488                          array('class' => 'responsehistoryheader'));
 489      }
 490  
 491      /**
 492       * Action author's profile link.
 493       *
 494       * @param question_attempt_step $step The step.
 495       * @param question_display_options $options The display options.
 496       * @return string The link to user's profile.
 497       */
 498      protected function action_author(question_attempt_step $step, question_display_options $options): string {
 499          if ($options->userinfoinhistory && $step->get_user_id() != $options->userinfoinhistory) {
 500              return html_writer::link(
 501                      new moodle_url('/user/view.php', ['id' => $step->get_user_id(), 'course' => $this->page->course->id]),
 502                      $step->get_user_fullname(), ['class' => 'd-table-cell']);
 503          } else {
 504              return '';
 505          }
 506      }
 507  }