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   * Page for editing questions.
  18   *
  19   * @package    qbank_editquestion
  20   * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  require_once(__DIR__ . '/../../../config.php');
  25  require_once (__DIR__ . '/../../editlib.php');
  26  require_once($CFG->libdir . '/filelib.php');
  27  require_once($CFG->libdir . '/formslib.php');
  28  
  29  // Read URL parameters telling us which question to edit.
  30  $id = optional_param('id', 0, PARAM_INT); // Question id.
  31  $makecopy = optional_param('makecopy', 0, PARAM_BOOL);
  32  $qtype = optional_param('qtype', '', PARAM_COMPONENT);
  33  $categoryid = optional_param('category', 0, PARAM_INT);
  34  $cmid = optional_param('cmid', 0, PARAM_INT);
  35  $courseid = optional_param('courseid', 0, PARAM_INT);
  36  $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA);
  37  $originalreturnurl = optional_param('returnurl', 0, PARAM_LOCALURL);
  38  $appendqnumstring = optional_param('appendqnumstring', '', PARAM_ALPHA);
  39  $inpopup = optional_param('inpopup', 0, PARAM_BOOL);
  40  $scrollpos = optional_param('scrollpos', 0, PARAM_INT);
  41  
  42  \core_question\local\bank\helper::require_plugin_enabled('qbank_editquestion');
  43  
  44  $url = new moodle_url('/question/bank/editquestion/question.php');
  45  if ($id !== 0) {
  46      $url->param('id', $id);
  47  }
  48  if ($makecopy) {
  49      $url->param('makecopy', $makecopy);
  50  }
  51  if ($qtype !== '') {
  52      $url->param('qtype', $qtype);
  53  }
  54  if ($categoryid !== 0) {
  55      $url->param('category', $categoryid);
  56  }
  57  if ($cmid !== 0) {
  58      $url->param('cmid', $cmid);
  59  }
  60  if ($courseid !== 0) {
  61      $url->param('courseid', $courseid);
  62  }
  63  if ($wizardnow !== '') {
  64      $url->param('wizardnow', $wizardnow);
  65  }
  66  if ($originalreturnurl !== 0) {
  67      $url->param('returnurl', $originalreturnurl);
  68  }
  69  if ($appendqnumstring !== '') {
  70      $url->param('appendqnumstring', $appendqnumstring);
  71  }
  72  if ($inpopup !== 0) {
  73      $url->param('inpopup', $inpopup);
  74  }
  75  if ($scrollpos) {
  76      $url->param('scrollpos', $scrollpos);
  77  }
  78  $PAGE->set_url($url);
  79  
  80  if ($cmid) {
  81      $questionbankurl = new moodle_url('/question/edit.php', array('cmid' => $cmid));
  82  } else {
  83      $questionbankurl = new moodle_url('/question/edit.php', array('courseid' => $courseid));
  84  }
  85  navigation_node::override_active_url($questionbankurl);
  86  
  87  if ($originalreturnurl) {
  88      if (strpos($originalreturnurl, '/') !== 0) {
  89          throw new coding_exception("returnurl must be a local URL starting with '/'. $originalreturnurl was given.");
  90      }
  91      $returnurl = new moodle_url($originalreturnurl);
  92  } else {
  93      $returnurl = $questionbankurl;
  94  }
  95  if ($scrollpos) {
  96      $returnurl->param('scrollpos', $scrollpos);
  97  }
  98  
  99  if ($cmid) {
 100      list($module, $cm) = get_module_from_cmid($cmid);
 101      require_login($cm->course, false, $cm);
 102      $thiscontext = context_module::instance($cmid);
 103  } else if ($courseid) {
 104      require_login($courseid, false);
 105      $thiscontext = context_course::instance($courseid);
 106      $module = null;
 107      $cm = null;
 108  } else {
 109      throw new moodle_exception('missingcourseorcmid', 'question');
 110  }
 111  $contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
 112  $PAGE->set_pagelayout('admin');
 113  
 114  if (optional_param('addcancel', false, PARAM_BOOL)) {
 115      redirect($returnurl);
 116  }
 117  
 118  if ($id) {
 119      if (!$question = $DB->get_record('question', array('id' => $id))) {
 120          throw new moodle_exception('questiondoesnotexist', 'question', $returnurl);
 121      }
 122      // We can use $COURSE here because it's been initialised as part of the
 123      // require_login above. Passing it as the third parameter tells the function
 124      // to filter the course tags by that course.
 125      get_question_options($question, true, [$COURSE]);
 126  
 127  } else if ($categoryid && $qtype) { // Only for creating new questions.
 128      $question = new stdClass();
 129      $question->category = $categoryid;
 130      $question->qtype = $qtype;
 131      $question->createdby = $USER->id;
 132  
 133      // Check that users are allowed to create this question type at the moment.
 134      if (!question_bank::qtype_enabled($qtype)) {
 135          throw new moodle_exception('cannotenable', 'question', $returnurl, $qtype);
 136      }
 137  
 138  } else if ($categoryid) {
 139      // Category, but no qtype. They probably came from the addquestion.php
 140      // script without choosing a question type. Send them back.
 141      $addurl = new moodle_url('/question/bank/editquestion/addquestion.php', $url->params());
 142      $addurl->param('validationerror', 1);
 143      redirect($addurl);
 144  
 145  } else {
 146      throw new moodle_exception('notenoughdatatoeditaquestion', 'question', $returnurl);
 147  }
 148  
 149  $qtypeobj = question_bank::get_qtype($question->qtype);
 150  
 151  if (isset($question->categoryobject)) {
 152      $category = $question->categoryobject;
 153  } else {
 154      // Validate the question category.
 155      if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
 156          throw new moodle_exception('categorydoesnotexist', 'question', $returnurl);
 157      }
 158  }
 159  
 160  // Check permissions.
 161  $question->formoptions = new stdClass();
 162  
 163  $categorycontext = context::instance_by_id($category->contextid);
 164  $question->contextid = $category->contextid;
 165  $addpermission = has_capability('moodle/question:add', $categorycontext);
 166  
 167  if ($id) {
 168      $question->formoptions->canedit = question_has_capability_on($question, 'edit');
 169      $question->formoptions->canmove = $addpermission && question_has_capability_on($question, 'move');
 170      $question->formoptions->cansaveasnew = $addpermission &&
 171              (question_has_capability_on($question, 'view') || $question->formoptions->canedit);
 172      $question->formoptions->repeatelements = $question->formoptions->canedit || $question->formoptions->cansaveasnew;
 173      $formeditable = $question->formoptions->canedit || $question->formoptions->cansaveasnew || $question->formoptions->canmove;
 174      if (!$formeditable) {
 175          question_require_capability_on($question, 'view');
 176      }
 177      $question->beingcopied = false;
 178      if ($makecopy) {
 179          // If we are duplicating a question, add some indication to the question name.
 180          $question->name = get_string('questionnamecopy', 'question', $question->name);
 181          $question->idnumber = isset($question->idnumber) ?
 182              core_question_find_next_unused_idnumber($question->idnumber, $category->id) : '';
 183          $question->beingcopied = true;
 184      }
 185  
 186  } else { // Creating a new question.
 187      $question->formoptions->canedit = question_has_capability_on($question, 'edit');
 188      $question->formoptions->canmove = (question_has_capability_on($question, 'move') && $addpermission);
 189      $question->formoptions->cansaveasnew = false;
 190      $question->formoptions->repeatelements = true;
 191      $formeditable = true;
 192      require_capability('moodle/question:add', $categorycontext);
 193  }
 194  $question->formoptions->mustbeusable = (bool) $appendqnumstring;
 195  
 196  // Validate the question type.
 197  $PAGE->set_pagetype('question-type-' . $question->qtype);
 198  
 199  // Create the question editing form.
 200  if ($wizardnow !== '') {
 201      $mform = $qtypeobj->next_wizard_form('question.php', $question, $wizardnow, $formeditable);
 202  } else {
 203      $mform = $qtypeobj->create_editing_form('question.php', $question, $category, $contexts, $formeditable);
 204  }
 205  $toform = fullclone($question); // Send the question object and a few more parameters to the form.
 206  $toform->category = "{$category->id},{$category->contextid}";
 207  $toform->scrollpos = $scrollpos;
 208  if ($formeditable && $id) {
 209      $toform->categorymoveto = $toform->category;
 210  }
 211  
 212  $toform->appendqnumstring = $appendqnumstring;
 213  $toform->returnurl = $originalreturnurl;
 214  $toform->makecopy = $makecopy;
 215  $toform->idnumber = null;
 216  if (!empty($question->id)) {
 217      $questionobject = question_bank::load_question($question->id);
 218      $toform->status = $questionobject->status;
 219      $toform->idnumber = $questionobject->idnumber;
 220  } else {
 221      $toform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
 222  }
 223  if ($makecopy) {
 224      $toform->idnumber = core_question_find_next_unused_idnumber($toform->idnumber, $category->id);
 225  }
 226  if ($cm !== null) {
 227      $toform->cmid = $cm->id;
 228      $toform->courseid = $cm->course;
 229  } else {
 230      $toform->courseid = $COURSE->id;
 231  }
 232  
 233  $toform->inpopup = $inpopup;
 234  
 235  // Prepare custom fields data.
 236  $customfieldhandler = qbank_customfields\customfield\question_handler::create();
 237  $customfieldhandler->instance_form_before_set_data($toform);
 238  
 239  $mform->set_data($toform);
 240  
 241  if ($mform->is_cancelled()) {
 242      if ($inpopup) {
 243          close_window();
 244      } else {
 245          redirect($returnurl);
 246      }
 247  
 248  } else if ($fromform = $mform->get_data()) {
 249      // If we are saving as a copy, break the connection to the old question.
 250      if ($makecopy) {
 251          $question->id = 0;
 252          // Copies should not be hidden.
 253          $question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
 254      }
 255  
 256      // If is will be added directly to a module send the module name to be referenced.
 257      if ($appendqnumstring && $cm) {
 258          $fromform->modulename = 'mod_' . $cm->modname;
 259      }
 260  
 261      // Process the combination of usecurrentcat, categorymoveto and category form
 262      // fields, so the save_question method only has to consider $fromform->category.
 263      if (empty($fromform->usecurrentcat) && !empty($fromform->categorymoveto)) {
 264          $fromform->category = $fromform->categorymoveto;
 265      }
 266  
 267      // If we are moving a question, check we have permission to move it from
 268      // whence it came (Where we are moving to is validated by the form).
 269      list($newcatid, $newcontextid) = explode(',', $fromform->category);
 270      if (!empty($question->id) && $newcatid != $question->categoryobject->id) {
 271          $contextid = $newcontextid;
 272          question_require_capability_on($question, 'move');
 273      } else {
 274          $contextid = $category->contextid;
 275      }
 276  
 277      // Ensure we redirect back to the category the question is being saved into.
 278      $returnurl->param('category', $fromform->category);
 279  
 280      // We are actually saving the question.
 281      if (!empty($question->id)) {
 282          question_require_capability_on($question, 'edit');
 283      } else {
 284          require_capability('moodle/question:add', context::instance_by_id($contextid));
 285          if (!empty($fromform->makecopy) && !$question->formoptions->cansaveasnew) {
 286              throw new moodle_exception('nopermissions', '', '', 'edit');
 287          }
 288      }
 289  
 290      // If this is a new question, save defaults for user in user_preferences table.
 291      if (empty($question->id)) {
 292          $qtypeobj->save_defaults_for_new_questions($fromform);
 293      }
 294      $question = $qtypeobj->save_question($question, $fromform);
 295      if (isset($fromform->tags)) {
 296          // If we have any question context level tags then set those tags now.
 297          core_tag_tag::set_item_tags('core_question', 'question', $question->id,
 298                  context::instance_by_id($contextid), $fromform->tags, 0);
 299      }
 300  
 301      if (isset($fromform->coursetags)) {
 302          // If we have and course context level tags then set those now.
 303          core_tag_tag::set_item_tags('core_question', 'question', $question->id,
 304                  context_course::instance($fromform->courseid), $fromform->coursetags, 0);
 305      }
 306  
 307      // Update custom fields if there are any of them in the form.
 308      $customfieldhandler->instance_form_save($fromform);
 309  
 310      // Purge this question from the cache.
 311      question_bank::notify_question_edited($question->id);
 312  
 313      // If we are saving and continuing to edit the question.
 314      if (!empty($fromform->updatebutton)) {
 315          $url->param('id', $question->id);
 316          $url->remove_params('makecopy');
 317          redirect($url);
 318      }
 319  
 320      if ($qtypeobj->finished_edit_wizard($fromform)) {
 321          if ($inpopup) {
 322              echo $OUTPUT->notification(get_string('changessaved'), '');
 323              close_window(3);
 324          } else {
 325              $returnurl->param('lastchanged', $question->id);
 326              if ($appendqnumstring) {
 327                  $returnurl->param($appendqnumstring, $question->id);
 328                  $returnurl->param('sesskey', sesskey());
 329                  $returnurl->param('cmid', $cmid);
 330              }
 331              redirect($returnurl);
 332          }
 333  
 334      } else {
 335          $nexturlparams = array(
 336                  'returnurl' => $originalreturnurl,
 337                  'appendqnumstring' => $appendqnumstring,
 338                  'scrollpos' => $scrollpos);
 339          if (isset($fromform->nextpageparam) && is_array($fromform->nextpageparam)) {
 340              // Useful for passing data to the next page which is not saved in the database.
 341              $nexturlparams += $fromform->nextpageparam;
 342          }
 343          $nexturlparams['id'] = $question->id;
 344          $nexturlparams['wizardnow'] = $fromform->wizard;
 345          $nexturl = new moodle_url($url, $nexturlparams);
 346          if ($cmid) {
 347              $nexturl->param('cmid', $cmid);
 348          } else {
 349              $nexturl->param('courseid', $COURSE->id);
 350          }
 351          redirect($nexturl);
 352      }
 353  
 354  }
 355  
 356  $streditingquestion = $qtypeobj->get_heading();
 357  $PAGE->set_title($streditingquestion);
 358  $PAGE->set_heading($COURSE->fullname);
 359  $PAGE->activityheader->disable();
 360  $PAGE->navbar->add($streditingquestion);
 361  if ($PAGE->course->id == $SITE->id) {
 362      $PAGE->set_primary_active_tab('home');
 363  }
 364  
 365  // Display a heading, question editing form and possibly some extra content needed for
 366  // for this question type.
 367  echo $OUTPUT->header();
 368  $qtypeobj->display_question_editing_page($mform, $question, $wizardnow);
 369  echo $OUTPUT->footer();