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 401 and 402] [Versions 401 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   * This page displays a preview of a question
  19   *
  20   * The preview uses the option settings from the activity within which the question
  21   * is previewed or the default settings if no activity is specified. The question session
  22   * information is stored in the session as an array of subsequent states rather
  23   * than in the database.
  24   *
  25   * @package    qbank_previewquestion
  26   * @copyright  Alex Smith {@link http://maths.york.ac.uk/serving_maths}
  27   * @author     2021 Safat Shahin <safatshahin@catalyst-au.net> and numerous contributors.
  28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  
  31  require_once(__DIR__ . '/../../../config.php');
  32  require_once($CFG->libdir . '/questionlib.php');
  33  
  34  use \core\notification;
  35  use qbank_previewquestion\form\preview_options_form;
  36  use qbank_previewquestion\question_preview_options;
  37  use qbank_previewquestion\helper;
  38  
  39  /**
  40   * The maximum number of variants previewable. If there are more variants than this for a question
  41   * then we only allow the selection of the first x variants.
  42   *
  43   * @var integer
  44   */
  45  define('QUESTION_PREVIEW_MAX_VARIANTS', 100);
  46  
  47  \core_question\local\bank\helper::require_plugin_enabled('qbank_previewquestion');
  48  
  49  // Get and validate question id.
  50  $id = required_param('id', PARAM_INT);
  51  $returnurl = optional_param('returnurl', null, PARAM_LOCALURL);
  52  $restartversion = optional_param('restartversion', question_preview_options::ALWAYS_LATEST, PARAM_INT);
  53  
  54  $question = question_bank::load_question($id);
  55  
  56  if ($returnurl) {
  57      $returnurl = new moodle_url($returnurl);
  58  }
  59  
  60  // Were we given a particular context to run the question in?
  61  // This affects things like filter settings, or forced theme or language.
  62  if ($cmid = optional_param('cmid', 0, PARAM_INT)) {
  63      $cm = get_coursemodule_from_id(false, $cmid);
  64      require_login($cm->course, false, $cm);
  65      $context = context_module::instance($cmid);
  66  
  67  } else if ($courseid = optional_param('courseid', 0, PARAM_INT)) {
  68      require_login($courseid);
  69      $context = context_course::instance($courseid);
  70  
  71  } else {
  72      require_login();
  73      $category = $DB->get_record('question_categories', ['id' => $question->category], '*', MUST_EXIST);
  74      $context = context::instance_by_id($category->contextid);
  75      $PAGE->set_context($context);
  76      // Note that in the other cases, require_login will set the correct page context.
  77  }
  78  question_require_capability_on($question, 'use');
  79  $PAGE->set_pagelayout('popup');
  80  
  81  // Get and validate display options.
  82  $maxvariant = min($question->get_num_variants(), QUESTION_PREVIEW_MAX_VARIANTS);
  83  $options = new question_preview_options($question);
  84  $options->load_user_defaults();
  85  $options->set_from_request();
  86  $PAGE->set_url(helper::question_preview_url($id, $options->behaviour, $options->maxmark,
  87          $options, $options->variant, $context, null, $restartversion));
  88  
  89  // Get and validate existing preview, or start a new one.
  90  $previewid = optional_param('previewid', 0, PARAM_INT);
  91  
  92  if ($previewid) {
  93      try {
  94          $quba = question_engine::load_questions_usage_by_activity($previewid);
  95  
  96      } catch (Exception $e) {
  97          // This may not seem like the right error message to display, but
  98          // actually from the user point of view, it makes sense.
  99          throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question',
 100                  helper::question_preview_url($question->id, $options->behaviour,
 101                          $options->maxmark, $options, $options->variant, $context, null, $restartversion), null, $e);
 102      }
 103  
 104      if ($quba->get_owning_context()->instanceid != $USER->id) {
 105          throw new moodle_exception('notyourpreview', 'question');
 106      }
 107  
 108      $slot = $quba->get_first_question_number();
 109      $usedquestion = $quba->get_question($slot, false);
 110      if ($usedquestion->id != $question->id) {
 111          throw new moodle_exception('questionidmismatch', 'question');
 112      }
 113      $question = $usedquestion;
 114      $options->variant = $quba->get_variant($slot);
 115  
 116  } else {
 117      $quba = question_engine::make_questions_usage_by_activity(
 118              'core_question_preview', context_user::instance($USER->id));
 119      $quba->set_preferred_behaviour($options->behaviour);
 120      $slot = $quba->add_question($question, $options->maxmark);
 121  
 122      if ($options->variant) {
 123          $options->variant = min($maxvariant, max(1, $options->variant));
 124      } else {
 125          $options->variant = rand(1, $maxvariant);
 126      }
 127  
 128      $quba->start_question($slot, $options->variant);
 129  
 130      $transaction = $DB->start_delegated_transaction();
 131      question_engine::save_questions_usage_by_activity($quba);
 132      $transaction->allow_commit();
 133  }
 134  $options->behaviour = $quba->get_preferred_behaviour();
 135  $options->maxmark = $quba->get_question_max_mark($slot);
 136  
 137  $versionids = helper::load_versions($question->questionbankentryid);
 138  // Create the settings form, and initialise the fields.
 139  $optionsform = new preview_options_form(helper::question_preview_form_url($question->id, $context, $previewid, $returnurl),
 140          [
 141              'quba' => $quba,
 142              'maxvariant' => $maxvariant,
 143              'versions' => array_combine(array_values($versionids), array_values($versionids)),
 144              'restartversion' => $restartversion,
 145          ]);
 146  $optionsform->set_data($options);
 147  
 148  // Process change of settings, if that was requested.
 149  if ($newoptions = $optionsform->get_submitted_data()) {
 150      // Set user preferences.
 151      $options->save_user_preview_options($newoptions);
 152      if (!isset($newoptions->variant)) {
 153          $newoptions->variant = $options->variant;
 154      }
 155      $questionid = helper::get_restart_id($versionids, $restartversion);
 156      if (isset($newoptions->saverestart)) {
 157          helper::restart_preview($previewid, $questionid, $newoptions, $context, $returnurl, $newoptions->restartversion);
 158      }
 159  }
 160  
 161  // Prepare a URL that is used in various places.
 162  $actionurl = helper::question_preview_action_url($question->id, $quba->get_id(), $options, $context, $returnurl, $restartversion);
 163  
 164  // Process any actions from the buttons at the bottom of the form.
 165  if (data_submitted() && confirm_sesskey()) {
 166  
 167      try {
 168  
 169          if (optional_param('restart', false, PARAM_BOOL)) {
 170              $questionid = helper::get_restart_id($versionids, $restartversion);
 171              helper::restart_preview($previewid, $questionid, $options, $context, $returnurl, $restartversion);
 172  
 173          } else if (optional_param('fill', null, PARAM_BOOL)) {
 174              $correctresponse = $quba->get_correct_response($slot);
 175              if (!is_null($correctresponse)) {
 176                  $quba->process_action($slot, $correctresponse);
 177  
 178                  $transaction = $DB->start_delegated_transaction();
 179                  question_engine::save_questions_usage_by_activity($quba);
 180                  $transaction->allow_commit();
 181              }
 182              redirect($actionurl);
 183  
 184          } else if (optional_param('finish', null, PARAM_BOOL)) {
 185              $quba->process_all_actions();
 186              $quba->finish_all_questions();
 187  
 188              $transaction = $DB->start_delegated_transaction();
 189              question_engine::save_questions_usage_by_activity($quba);
 190              $transaction->allow_commit();
 191              redirect($actionurl);
 192  
 193          } else {
 194              $quba->process_all_actions();
 195  
 196              $transaction = $DB->start_delegated_transaction();
 197              question_engine::save_questions_usage_by_activity($quba);
 198              $transaction->allow_commit();
 199  
 200              $scrollpos = optional_param('scrollpos', '', PARAM_RAW);
 201              if ($scrollpos !== '') {
 202                  $actionurl->param('scrollpos', (int) $scrollpos);
 203              }
 204              redirect($actionurl);
 205          }
 206  
 207      } catch (question_out_of_sequence_exception $e) {
 208          throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', $actionurl);
 209  
 210      } catch (Exception $e) {
 211          // This sucks, if we display our own custom error message, there is no way
 212          // to display the original stack trace.
 213          $debuginfo = '';
 214          if (!empty($e->debuginfo)) {
 215              $debuginfo = $e->debuginfo;
 216          }
 217          throw new moodle_exception('errorprocessingresponses', 'question', $actionurl,
 218                  $e->getMessage(), $debuginfo);
 219      }
 220  }
 221  
 222  if ($question->length) {
 223      $displaynumber = '1';
 224  } else {
 225      $displaynumber = 'i';
 226  }
 227  $restartdisabled = [];
 228  $finishdisabled = [];
 229  $filldisabled = [];
 230  if ($quba->get_question_state($slot)->is_finished()) {
 231      $finishdisabled = ['disabled' => 'disabled'];
 232      $filldisabled = ['disabled' => 'disabled'];
 233  }
 234  // If question type cannot give us a correct response, disable this button.
 235  if (is_null($quba->get_correct_response($slot))) {
 236      $filldisabled = ['disabled' => 'disabled'];
 237  }
 238  if (!$previewid) {
 239      $restartdisabled = ['disabled' => 'disabled'];
 240  }
 241  
 242  // Prepare technical info to be output.
 243  $qa = $quba->get_question_attempt($slot);
 244  $technical = [];
 245  $technical[] = get_string('behaviourbeingused', 'question',
 246          question_engine::get_behaviour_name($qa->get_behaviour_name()));
 247  $technical[] = get_string('technicalinfominfraction', 'question', $qa->get_min_fraction());
 248  $technical[] = get_string('technicalinfomaxfraction', 'question', $qa->get_max_fraction());
 249  $technical[] = get_string('technicalinfovariant', 'question', $qa->get_variant());
 250  $technical[] = get_string('technicalinfoquestionsummary', 'question', s($qa->get_question_summary()));
 251  $technical[] = get_string('technicalinforightsummary', 'question', s($qa->get_right_answer_summary()));
 252  $technical[] = get_string('technicalinforesponsesummary', 'question', s($qa->get_response_summary()));
 253  $technical[] = get_string('technicalinfostate', 'question', '' . $qa->get_state());
 254  
 255  // Start output.
 256  $title = get_string('previewquestion', 'question', format_string($question->name));
 257  $headtags = question_engine::initialise_js() . $quba->render_question_head_html($slot);
 258  $PAGE->set_title($title);
 259  $PAGE->set_heading($title);
 260  echo $OUTPUT->header();
 261  
 262  $previewdata = [];
 263  
 264  $previewdata['questionicon'] = print_question_icon($question);
 265  $previewdata['questionidumber'] = $question->idnumber;
 266  $previewdata['questiontitle'] = $question->name;
 267  $islatestversion = is_latest($question->version, $question->questionbankentryid);
 268  if ($islatestversion) {
 269      $previewdata['versiontitle'] = get_string('versiontitlelatest', 'qbank_previewquestion', $question->version);
 270  } else {
 271      $previewdata['versiontitle'] = get_string('versiontitle', 'qbank_previewquestion', $question->version);
 272      if ($restartversion == question_preview_options::ALWAYS_LATEST) {
 273          $newerversionparams = (object) [
 274              'currentversion' => $question->version,
 275              'latestversion' => max($versionids),
 276              'restartbutton' => $OUTPUT->render_from_template('qbank_previewquestion/restartbutton', []),
 277          ];
 278          $newversionurl = clone $actionurl;
 279          $newversionurl->param('restart', 1);
 280          $previewdata['newerversionurl'] = $newversionurl;
 281          $previewdata['newerversion'] = get_string('newerversion', 'qbank_previewquestion', $newerversionparams);
 282      }
 283  }
 284  
 285  $previewdata['actionurl'] = $actionurl;
 286  $previewdata['session'] = sesskey();
 287  $previewdata['slot'] = $slot;
 288  // Output of the question.
 289  $previewdata['question'] = $quba->render_question($slot, $options, $displaynumber);
 290  $previewdata['restartdisabled'] = html_writer::attributes($restartdisabled);
 291  $previewdata['finishdisabled'] = html_writer::attributes($finishdisabled);
 292  $previewdata['filldisabled'] = html_writer::attributes($filldisabled);
 293  // Output the technical info.
 294  $previewdata['techinfo'] = print_collapsible_region_start('', 'techinfo', get_string('technicalinfo', 'question'),
 295          'core_question_preview_techinfo_collapsed', true, true, $OUTPUT->help_icon('technicalinfo', 'question'));
 296  foreach ($technical as $info) {
 297      $previewdata['techinfo'] .= html_writer::tag('p', $info, ['class' => 'notifytiny']);
 298  }
 299  $previewdata['techinfo'] .= print_collapsible_region_end(true);
 300  
 301  // Display the settings form.
 302  $previewdata['options'] = $optionsform->render();
 303  
 304  list($comment, $extraelements) = helper::get_preview_extra_elements($question, $COURSE->id);
 305  
 306  if (!empty($comment)) {
 307      $previewdata['comments'] = $comment;
 308  }
 309  
 310  if (!empty($extraelements)) {
 311      $elements = [];
 312      foreach ($extraelements as $extraelement) {
 313          $element = new stdClass();
 314          $element->extrapreviewelements = $extraelement;
 315          $elements[] = $element;
 316      }
 317      $previewdata['extrapreviewelements'] = $elements;
 318  }
 319  
 320  $previewdata['redirect'] = false;
 321  if (!is_null($returnurl)) {
 322      $previewdata['redirect'] = true;
 323      $previewdata['redirecturl'] = $returnurl;
 324  }
 325  $closeurl = new moodle_url('/question/edit.php', ['courseid' => $COURSE->id]);
 326  echo $PAGE->get_renderer('qbank_previewquestion')->render_preview_page($previewdata);
 327  
 328  // Log the preview of this question.
 329  $event = \core\event\question_viewed::create_from_question_instance($question, $context);
 330  $event->trigger();
 331  
 332  $PAGE->requires->js_call_amd('qbank_previewquestion/preview', 'init', [$previewdata['redirect'], $closeurl->__toString()]);
 333  echo $OUTPUT->footer();