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 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   * 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    moodlecore
  26   * @subpackage questionengine
  27   * @copyright  Alex Smith {@link http://maths.york.ac.uk/serving_maths} and
  28   *      numerous contributors.
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  
  32  
  33  require_once(__DIR__ . '/../config.php');
  34  require_once($CFG->libdir . '/questionlib.php');
  35  require_once (__DIR__ . '/previewlib.php');
  36  
  37  /**
  38   * The maximum number of variants previewable. If there are more variants than this for a question
  39   * then we only allow the selection of the first x variants.
  40   * @var integer
  41   */
  42  define('QUESTION_PREVIEW_MAX_VARIANTS', 100);
  43  
  44  // Get and validate question id.
  45  $id = required_param('id', PARAM_INT);
  46  $question = question_bank::load_question($id);
  47  
  48  // Were we given a particular context to run the question in?
  49  // This affects things like filter settings, or forced theme or language.
  50  if ($cmid = optional_param('cmid', 0, PARAM_INT)) {
  51      $cm = get_coursemodule_from_id(false, $cmid);
  52      require_login($cm->course, false, $cm);
  53      $context = context_module::instance($cmid);
  54  
  55  } else if ($courseid = optional_param('courseid', 0, PARAM_INT)) {
  56      require_login($courseid);
  57      $context = context_course::instance($courseid);
  58  
  59  } else {
  60      require_login();
  61      $category = $DB->get_record('question_categories',
  62              array('id' => $question->category), '*', MUST_EXIST);
  63      $context = context::instance_by_id($category->contextid);
  64      $PAGE->set_context($context);
  65      // Note that in the other cases, require_login will set the correct page context.
  66  }
  67  question_require_capability_on($question, 'use');
  68  $PAGE->set_pagelayout('popup');
  69  
  70  // Get and validate display options.
  71  $maxvariant = min($question->get_num_variants(), QUESTION_PREVIEW_MAX_VARIANTS);
  72  $options = new question_preview_options($question);
  73  $options->load_user_defaults();
  74  $options->set_from_request();
  75  $PAGE->set_url(question_preview_url($id, $options->behaviour, $options->maxmark,
  76          $options, $options->variant, $context));
  77  
  78  // Get and validate existing preview, or start a new one.
  79  $previewid = optional_param('previewid', 0, PARAM_INT);
  80  
  81  if ($previewid) {
  82      try {
  83          $quba = question_engine::load_questions_usage_by_activity($previewid);
  84  
  85      } catch (Exception $e) {
  86          // This may not seem like the right error message to display, but
  87          // actually from the user point of view, it makes sense.
  88          print_error('submissionoutofsequencefriendlymessage', 'question',
  89                  question_preview_url($question->id, $options->behaviour,
  90                  $options->maxmark, $options, $options->variant, $context), null, $e);
  91      }
  92  
  93      if ($quba->get_owning_context()->instanceid != $USER->id) {
  94          print_error('notyourpreview', 'question');
  95      }
  96  
  97      $slot = $quba->get_first_question_number();
  98      $usedquestion = $quba->get_question($slot, false);
  99      if ($usedquestion->id != $question->id) {
 100          print_error('questionidmismatch', 'question');
 101      }
 102      $question = $usedquestion;
 103      $options->variant = $quba->get_variant($slot);
 104  
 105  } else {
 106      $quba = question_engine::make_questions_usage_by_activity(
 107              'core_question_preview', context_user::instance($USER->id));
 108      $quba->set_preferred_behaviour($options->behaviour);
 109      $slot = $quba->add_question($question, $options->maxmark);
 110  
 111      if ($options->variant) {
 112          $options->variant = min($maxvariant, max(1, $options->variant));
 113      } else {
 114          $options->variant = rand(1, $maxvariant);
 115      }
 116  
 117      $quba->start_question($slot, $options->variant);
 118  
 119      $transaction = $DB->start_delegated_transaction();
 120      question_engine::save_questions_usage_by_activity($quba);
 121      $transaction->allow_commit();
 122  }
 123  $options->behaviour = $quba->get_preferred_behaviour();
 124  $options->maxmark = $quba->get_question_max_mark($slot);
 125  
 126  // Create the settings form, and initialise the fields.
 127  $optionsform = new preview_options_form(question_preview_form_url($question->id, $context, $previewid),
 128          array('quba' => $quba, 'maxvariant' => $maxvariant));
 129  $optionsform->set_data($options);
 130  
 131  // Process change of settings, if that was requested.
 132  if ($newoptions = $optionsform->get_submitted_data()) {
 133      // Set user preferences.
 134      $options->save_user_preview_options($newoptions);
 135      if (!isset($newoptions->variant)) {
 136          $newoptions->variant = $options->variant;
 137      }
 138      if (isset($newoptions->saverestart)) {
 139          restart_preview($previewid, $question->id, $newoptions, $context);
 140      }
 141  }
 142  
 143  // Prepare a URL that is used in various places.
 144  $actionurl = question_preview_action_url($question->id, $quba->get_id(), $options, $context);
 145  
 146  // Process any actions from the buttons at the bottom of the form.
 147  if (data_submitted() && confirm_sesskey()) {
 148  
 149      try {
 150  
 151          if (optional_param('restart', false, PARAM_BOOL)) {
 152              restart_preview($previewid, $question->id, $options, $context);
 153  
 154          } else if (optional_param('fill', null, PARAM_BOOL)) {
 155              $correctresponse = $quba->get_correct_response($slot);
 156              if (!is_null($correctresponse)) {
 157                  $quba->process_action($slot, $correctresponse);
 158  
 159                  $transaction = $DB->start_delegated_transaction();
 160                  question_engine::save_questions_usage_by_activity($quba);
 161                  $transaction->allow_commit();
 162              }
 163              redirect($actionurl);
 164  
 165          } else if (optional_param('finish', null, PARAM_BOOL)) {
 166              $quba->process_all_actions();
 167              $quba->finish_all_questions();
 168  
 169              $transaction = $DB->start_delegated_transaction();
 170              question_engine::save_questions_usage_by_activity($quba);
 171              $transaction->allow_commit();
 172              redirect($actionurl);
 173  
 174          } else {
 175              $quba->process_all_actions();
 176  
 177              $transaction = $DB->start_delegated_transaction();
 178              question_engine::save_questions_usage_by_activity($quba);
 179              $transaction->allow_commit();
 180  
 181              $scrollpos = optional_param('scrollpos', '', PARAM_RAW);
 182              if ($scrollpos !== '') {
 183                  $actionurl->param('scrollpos', (int) $scrollpos);
 184              }
 185              redirect($actionurl);
 186          }
 187  
 188      } catch (question_out_of_sequence_exception $e) {
 189          print_error('submissionoutofsequencefriendlymessage', 'question', $actionurl);
 190  
 191      } catch (Exception $e) {
 192          // This sucks, if we display our own custom error message, there is no way
 193          // to display the original stack trace.
 194          $debuginfo = '';
 195          if (!empty($e->debuginfo)) {
 196              $debuginfo = $e->debuginfo;
 197          }
 198          print_error('errorprocessingresponses', 'question', $actionurl,
 199                  $e->getMessage(), $debuginfo);
 200      }
 201  }
 202  
 203  if ($question->length) {
 204      $displaynumber = '1';
 205  } else {
 206      $displaynumber = 'i';
 207  }
 208  $restartdisabled = array();
 209  $finishdisabled = array();
 210  $filldisabled = array();
 211  if ($quba->get_question_state($slot)->is_finished()) {
 212      $finishdisabled = array('disabled' => 'disabled');
 213      $filldisabled = array('disabled' => 'disabled');
 214  }
 215  // If question type cannot give us a correct response, disable this button.
 216  if (is_null($quba->get_correct_response($slot))) {
 217      $filldisabled = array('disabled' => 'disabled');
 218  }
 219  if (!$previewid) {
 220      $restartdisabled = array('disabled' => 'disabled');
 221  }
 222  
 223  // Prepare technical info to be output.
 224  $qa = $quba->get_question_attempt($slot);
 225  $technical = array();
 226  $technical[] = get_string('behaviourbeingused', 'question',
 227          question_engine::get_behaviour_name($qa->get_behaviour_name()));
 228  $technical[] = get_string('technicalinfominfraction',     'question', $qa->get_min_fraction());
 229  $technical[] = get_string('technicalinfomaxfraction',     'question', $qa->get_max_fraction());
 230  $technical[] = get_string('technicalinfovariant',         'question', $qa->get_variant());
 231  $technical[] = get_string('technicalinfoquestionsummary', 'question', s($qa->get_question_summary()));
 232  $technical[] = get_string('technicalinforightsummary',    'question', s($qa->get_right_answer_summary()));
 233  $technical[] = get_string('technicalinforesponsesummary', 'question', s($qa->get_response_summary()));
 234  $technical[] = get_string('technicalinfostate',           'question', '' . $qa->get_state());
 235  
 236  // Start output.
 237  $title = get_string('previewquestion', 'question', format_string($question->name));
 238  $headtags = question_engine::initialise_js() . $quba->render_question_head_html($slot);
 239  $PAGE->set_title($title);
 240  $PAGE->set_heading($title);
 241  echo $OUTPUT->header();
 242  
 243  // Start the question form.
 244  echo html_writer::start_tag('form', array('method' => 'post', 'action' => $actionurl,
 245          'enctype' => 'multipart/form-data', 'id' => 'responseform'));
 246  echo html_writer::start_tag('div');
 247  echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
 248  echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', 'value' => $slot));
 249  echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos', 'value' => '', 'id' => 'scrollpos'));
 250  echo html_writer::end_tag('div');
 251  
 252  // Output the question.
 253  echo $quba->render_question($slot, $options, $displaynumber);
 254  
 255  // Finish the question form.
 256  echo html_writer::start_tag('div', array('id' => 'previewcontrols', 'class' => 'controls'));
 257  echo html_writer::empty_tag('input', $restartdisabled + array('type' => 'submit',
 258          'name' => 'restart', 'value' => get_string('restart', 'question'), 'class' => 'btn btn-secondary mr-1 mb-1',
 259          'id' => 'id_restart_question_preview'));
 260  echo html_writer::empty_tag('input', $finishdisabled + array('type' => 'submit',
 261          'name' => 'save', 'value' => get_string('save', 'question'), 'class' => 'btn btn-secondary mr-1 mb-1',
 262          'id' => 'id_save_question_preview'));
 263  echo html_writer::empty_tag('input', $filldisabled    + array('type' => 'submit',
 264          'name' => 'fill',    'value' => get_string('fillincorrect', 'question'), 'class' => 'btn btn-secondary mr-1 mb-1'));
 265  echo html_writer::empty_tag('input', $finishdisabled + array('type' => 'submit',
 266          'name' => 'finish', 'value' => get_string('submitandfinish', 'question'), 'class' => 'btn btn-secondary mr-1 mb-1',
 267          'id' => 'id_finish_question_preview'));
 268  echo html_writer::end_tag('div');
 269  echo html_writer::end_tag('form');
 270  
 271  // Output the technical info.
 272  print_collapsible_region_start('', 'techinfo', get_string('technicalinfo', 'question'),
 273          'core_question_preview_techinfo_collapsed', true, false, $OUTPUT->help_icon('technicalinfo', 'question'));
 274  foreach ($technical as $info) {
 275      echo html_writer::tag('p', $info, array('class' => 'notifytiny'));
 276  }
 277  print_collapsible_region_end();
 278  
 279  // Output a link to export this single question.
 280  if (question_has_capability_on($question, 'view')) {
 281      echo html_writer::link(question_get_export_single_question_url($question),
 282              get_string('exportonequestion', 'question'));
 283  }
 284  
 285  // Log the preview of this question.
 286  $event = \core\event\question_viewed::create_from_question_instance($question, $context);
 287  $event->trigger();
 288  
 289  // Display the settings form.
 290  $optionsform->display();
 291  
 292  $PAGE->requires->js_module('core_question_engine');
 293  $PAGE->requires->strings_for_js(array(
 294      'closepreview',
 295  ), 'question');
 296  $PAGE->requires->yui_module('moodle-question-preview', 'M.question.preview.init');
 297  $PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_save_question_preview']);
 298  $PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_finish_question_preview']);
 299  $PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_restart_question_preview']);
 300  $PAGE->requires->yui_module('moodle-core-formchangechecker',
 301      'M.core_formchangechecker.init', [['formid' => 'responseform']]);
 302  echo $OUTPUT->footer();
 303