Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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  $options->versioninfo = false;
  87  $PAGE->set_url(helper::question_preview_url($id, $options->behaviour, $options->maxmark,
  88          $options, $options->variant, $context, null, $restartversion));
  89  
  90  // Get and validate existing preview, or start a new one.
  91  $previewid = optional_param('previewid', 0, PARAM_INT);
  92  
  93  if ($previewid) {
  94      try {
  95          $quba = question_engine::load_questions_usage_by_activity($previewid);
  96  
  97      } catch (Exception $e) {
  98          // This may not seem like the right error message to display, but
  99          // actually from the user point of view, it makes sense.
 100          throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question',
 101                  helper::question_preview_url($question->id, $options->behaviour,
 102                          $options->maxmark, $options, $options->variant, $context, null, $restartversion), null, $e);
 103      }
 104  
 105      if ($quba->get_owning_context()->instanceid != $USER->id) {
 106          throw new moodle_exception('notyourpreview', 'question');
 107      }
 108  
 109      $slot = $quba->get_first_question_number();
 110      $usedquestion = $quba->get_question($slot, false);
 111      if ($usedquestion->id != $question->id) {
 112          throw new moodle_exception('questionidmismatch', 'question');
 113      }
 114      $question = $usedquestion;
 115      $options->variant = $quba->get_variant($slot);
 116  
 117  } else {
 118      $quba = question_engine::make_questions_usage_by_activity(
 119              'core_question_preview', context_user::instance($USER->id));
 120      $quba->set_preferred_behaviour($options->behaviour);
 121      $slot = $quba->add_question($question, $options->maxmark);
 122  
 123      if ($options->variant) {
 124          $options->variant = min($maxvariant, max(1, $options->variant));
 125      } else {
 126          $options->variant = rand(1, $maxvariant);
 127      }
 128  
 129      $quba->start_question($slot, $options->variant);
 130  
 131      $transaction = $DB->start_delegated_transaction();
 132      question_engine::save_questions_usage_by_activity($quba);
 133      $transaction->allow_commit();
 134  }
 135  $options->behaviour = $quba->get_preferred_behaviour();
 136  $options->maxmark = $quba->get_question_max_mark($slot);
 137  
 138  $versionids = helper::load_versions($question->questionbankentryid);
 139  // Create the settings form, and initialise the fields.
 140  $optionsform = new preview_options_form(helper::question_preview_form_url($question->id, $context, $previewid, $returnurl),
 141          [
 142              'quba' => $quba,
 143              'maxvariant' => $maxvariant,
 144              'versions' => array_combine(array_values($versionids), array_values($versionids)),
 145              'restartversion' => $restartversion,
 146          ]);
 147  $optionsform->set_data($options);
 148  
 149  // Process change of settings, if that was requested.
 150  if ($newoptions = $optionsform->get_submitted_data()) {
 151      // Set user preferences.
 152      $options->save_user_preview_options($newoptions);
 153      if (!isset($newoptions->variant)) {
 154          $newoptions->variant = $options->variant;
 155      }
 156      $questionid = helper::get_restart_id($versionids, $restartversion);
 157      if (isset($newoptions->saverestart)) {
 158          helper::restart_preview($previewid, $questionid, $newoptions, $context, $returnurl, $newoptions->restartversion);
 159      }
 160  }
 161  
 162  // Prepare a URL that is used in various places.
 163  $actionurl = helper::question_preview_action_url($question->id, $quba->get_id(), $options, $context, $returnurl, $restartversion);
 164  
 165  // Process any actions from the buttons at the bottom of the form.
 166  if (data_submitted() && confirm_sesskey()) {
 167  
 168      try {
 169  
 170          if (optional_param('restart', false, PARAM_BOOL)) {
 171              $questionid = helper::get_restart_id($versionids, $restartversion);
 172              helper::restart_preview($previewid, $questionid, $options, $context, $returnurl, $restartversion);
 173  
 174          } else if (optional_param('fill', null, PARAM_BOOL)) {
 175              $correctresponse = $quba->get_correct_response($slot);
 176              if (!is_null($correctresponse)) {
 177                  $quba->process_action($slot, $correctresponse);
 178  
 179                  $transaction = $DB->start_delegated_transaction();
 180                  question_engine::save_questions_usage_by_activity($quba);
 181                  $transaction->allow_commit();
 182              }
 183              redirect($actionurl);
 184  
 185          } else if (optional_param('finish', null, PARAM_BOOL)) {
 186              $quba->process_all_actions();
 187              $quba->finish_all_questions();
 188  
 189              $transaction = $DB->start_delegated_transaction();
 190              question_engine::save_questions_usage_by_activity($quba);
 191              $transaction->allow_commit();
 192              redirect($actionurl);
 193  
 194          } else {
 195              $quba->process_all_actions();
 196  
 197              $transaction = $DB->start_delegated_transaction();
 198              question_engine::save_questions_usage_by_activity($quba);
 199              $transaction->allow_commit();
 200  
 201              $mdlscrollto = optional_param('mdlscrollto', '', PARAM_RAW);
 202              if ($mdlscrollto !== '') {
 203                  $actionurl->param('mdlscrollto', (int) $mdlscrollto);
 204              }
 205              redirect($actionurl);
 206          }
 207  
 208      } catch (question_out_of_sequence_exception $e) {
 209          throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', $actionurl);
 210  
 211      } catch (Exception $e) {
 212          // This sucks, if we display our own custom error message, there is no way
 213          // to display the original stack trace.
 214          $debuginfo = '';
 215          if (!empty($e->debuginfo)) {
 216              $debuginfo = $e->debuginfo;
 217          }
 218          throw new moodle_exception('errorprocessingresponses', 'question', $actionurl,
 219                  $e->getMessage(), $debuginfo);
 220      }
 221  }
 222  
 223  if ($question->length) {
 224      $displaynumber = '1';
 225  } else {
 226      $displaynumber = 'i';
 227  }
 228  $restartdisabled = [];
 229  $finishdisabled = [];
 230  $filldisabled = [];
 231  if ($quba->get_question_state($slot)->is_finished()) {
 232      $finishdisabled = ['disabled' => 'disabled'];
 233      $filldisabled = ['disabled' => 'disabled'];
 234  }
 235  // If question type cannot give us a correct response, disable this button.
 236  if (is_null($quba->get_correct_response($slot))) {
 237      $filldisabled = ['disabled' => 'disabled'];
 238  }
 239  if (!$previewid) {
 240      $restartdisabled = ['disabled' => 'disabled'];
 241  }
 242  
 243  // Prepare technical info to be output.
 244  $qa = $quba->get_question_attempt($slot);
 245  $technical = [];
 246  $technical[] = get_string('behaviourbeingused', 'question',
 247          question_engine::get_behaviour_name($qa->get_behaviour_name()));
 248  $technical[] = get_string('technicalinfominfraction', 'question', $qa->get_min_fraction());
 249  $technical[] = get_string('technicalinfomaxfraction', 'question', $qa->get_max_fraction());
 250  $technical[] = get_string('technicalinfovariant', 'question', $qa->get_variant());
 251  $technical[] = get_string('technicalinfoquestionsummary', 'question', s($qa->get_question_summary()));
 252  $technical[] = get_string('technicalinforightsummary', 'question', s($qa->get_right_answer_summary()));
 253  $technical[] = get_string('technicalinforesponsesummary', 'question', s($qa->get_response_summary()));
 254  $technical[] = get_string('technicalinfostate', 'question', '' . $qa->get_state());
 255  
 256  // Start output.
 257  $title = get_string('previewquestion', 'question', format_string($question->name));
 258  $headtags = question_engine::initialise_js() . $quba->render_question_head_html($slot);
 259  $PAGE->set_title($title);
 260  $PAGE->set_heading($title);
 261  echo $OUTPUT->header();
 262  
 263  $previewdata = [];
 264  
 265  $previewdata['questionicon'] = print_question_icon($question);
 266  $previewdata['questionidumber'] = $question->idnumber;
 267  $previewdata['questiontitle'] = $question->name;
 268  $versioninfo = new \core_question\output\question_version_info($question);
 269  $previewdata['versiontitle'] = $versioninfo->export_for_template($OUTPUT);
 270  if ($versioninfo->version !== $versioninfo->latestversion) {
 271      if ($restartversion == question_preview_options::ALWAYS_LATEST) {
 272          $newerversionparams = (object) [
 273              'currentversion' => $question->version,
 274              'latestversion' => max($versionids),
 275              'restartbutton' => $OUTPUT->render_from_template('qbank_previewquestion/restartbutton', []),
 276          ];
 277          $newversionurl = clone $actionurl;
 278          $newversionurl->param('restart', 1);
 279          $previewdata['newerversionurl'] = $newversionurl;
 280          $previewdata['newerversion'] = get_string('newerversion', 'qbank_previewquestion', $newerversionparams);
 281      }
 282  }
 283  
 284  $previewdata['actionurl'] = $actionurl;
 285  $previewdata['session'] = sesskey();
 286  $previewdata['slot'] = $slot;
 287  // Output of the question.
 288  $previewdata['question'] = $quba->render_question($slot, $options, $displaynumber);
 289  $previewdata['restartdisabled'] = html_writer::attributes($restartdisabled);
 290  $previewdata['finishdisabled'] = html_writer::attributes($finishdisabled);
 291  $previewdata['filldisabled'] = html_writer::attributes($filldisabled);
 292  // Output the technical info.
 293  $previewdata['techinfo'] = print_collapsible_region_start('', 'techinfo', get_string('technicalinfo', 'question'),
 294          'core_question_preview_techinfo_collapsed', true, true, $OUTPUT->help_icon('technicalinfo', 'question'));
 295  foreach ($technical as $info) {
 296      $previewdata['techinfo'] .= html_writer::tag('p', $info, ['class' => 'notifytiny']);
 297  }
 298  $previewdata['techinfo'] .= print_collapsible_region_end(true);
 299  
 300  // Display the settings form.
 301  $previewdata['options'] = $optionsform->render();
 302  
 303  list($comment, $extraelements) = helper::get_preview_extra_elements($question, $COURSE->id);
 304  
 305  if (!empty($comment)) {
 306      $previewdata['comments'] = $comment;
 307  }
 308  
 309  if (!empty($extraelements)) {
 310      $elements = [];
 311      foreach ($extraelements as $extraelement) {
 312          $element = new stdClass();
 313          $element->extrapreviewelements = $extraelement;
 314          $elements[] = $element;
 315      }
 316      $previewdata['extrapreviewelements'] = $elements;
 317  }
 318  
 319  $previewdata['redirect'] = false;
 320  if (!is_null($returnurl)) {
 321      $previewdata['redirect'] = true;
 322      $previewdata['redirecturl'] = $returnurl;
 323  }
 324  $closeurl = new moodle_url('/question/edit.php', ['courseid' => $COURSE->id]);
 325  echo $PAGE->get_renderer('qbank_previewquestion')->render_preview_page($previewdata);
 326  
 327  // Log the preview of this question.
 328  $event = \core\event\question_viewed::create_from_question_instance($question, $context);
 329  $event->trigger();
 330  
 331  $PAGE->requires->js_call_amd('qbank_previewquestion/preview', 'init', [$previewdata['redirect'], $closeurl->__toString()]);
 332  echo $OUTPUT->footer();