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 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [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   * Renderer outputting the quiz editing UI.
  19   *
  20   * @package mod_quiz
  21   * @copyright 2013 The Open University.
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace mod_quiz\output;
  26  
  27  use core_question\local\bank\question_version_status;
  28  use mod_quiz\question\bank\qbank_helper;
  29  use \mod_quiz\structure;
  30  use \html_writer;
  31  use qbank_previewquestion\question_preview_options;
  32  use renderable;
  33  
  34  /**
  35   * Renderer outputting the quiz editing UI.
  36   *
  37   * @copyright 2013 The Open University.
  38   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   * @since Moodle 2.7
  40   */
  41  class edit_renderer extends \plugin_renderer_base {
  42  
  43      /** @var string The toggle group name of the checkboxes for the toggle-all functionality. */
  44      protected $togglegroup = 'quiz-questions';
  45  
  46      /**
  47       * Render the edit page
  48       *
  49       * @param \mod_quiz\quiz_settings $quizobj object containing all the quiz settings information.
  50       * @param structure $structure object containing the structure of the quiz.
  51       * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
  52       * @param \moodle_url $pageurl the canonical URL of this page.
  53       * @param array $pagevars the variables from {@link question_edit_setup()}.
  54       * @return string HTML to output.
  55       */
  56      public function edit_page(\mod_quiz\quiz_settings $quizobj, structure $structure,
  57          \core_question\local\bank\question_edit_contexts $contexts, \moodle_url $pageurl, array $pagevars) {
  58          $output = '';
  59  
  60          // Page title.
  61          $output .= $this->heading(get_string('questions', 'quiz'));
  62  
  63          // Information at the top.
  64          $output .= $this->quiz_state_warnings($structure);
  65  
  66          $output .= html_writer::start_div('mod_quiz-edit-top-controls');
  67  
  68          $output .= html_writer::start_div('d-flex justify-content-between flex-wrap mb-1');
  69          $output .= html_writer::start_div('d-flex flex-column justify-content-around');
  70          $output .= $this->quiz_information($structure);
  71          $output .= html_writer::end_tag('div');
  72          $output .= $this->maximum_grade_input($structure, $pageurl);
  73          $output .= html_writer::end_tag('div');
  74  
  75          $output .= html_writer::start_div('d-flex justify-content-between flex-wrap mb-1');
  76          $output .= html_writer::start_div('mod_quiz-edit-action-buttons btn-group edit-toolbar', ['role' => 'group']);
  77          $output .= $this->repaginate_button($structure, $pageurl);
  78          $output .= $this->selectmultiple_button($structure);
  79          $output .= html_writer::end_tag('div');
  80  
  81          $output .= html_writer::start_div('d-flex flex-column justify-content-around');
  82          $output .= $this->total_marks($quizobj->get_quiz());
  83          $output .= html_writer::end_tag('div');
  84          $output .= html_writer::end_tag('div');
  85  
  86          $output .= $this->selectmultiple_controls($structure);
  87          $output .= html_writer::end_tag('div');
  88  
  89          // Show the questions organised into sections and pages.
  90          $output .= $this->start_section_list($structure);
  91  
  92          foreach ($structure->get_sections() as $section) {
  93              $output .= $this->start_section($structure, $section);
  94              $output .= $this->questions_in_section($structure, $section, $contexts, $pagevars, $pageurl);
  95  
  96              if ($structure->is_last_section($section)) {
  97                  $output .= \html_writer::start_div('last-add-menu');
  98                  $output .= html_writer::tag('span', $this->add_menu_actions($structure, 0,
  99                          $pageurl, $contexts, $pagevars), ['class' => 'add-menu-outer']);
 100                  $output .= \html_writer::end_div();
 101              }
 102  
 103              $output .= $this->end_section();
 104          }
 105  
 106          $output .= $this->end_section_list();
 107  
 108          // Initialise the JavaScript.
 109          $this->initialise_editing_javascript($structure, $contexts, $pagevars, $pageurl);
 110  
 111          // Include the contents of any other popups required.
 112          if ($structure->can_be_edited()) {
 113              $thiscontext = $contexts->lowest();
 114              $this->page->requires->js_call_amd('mod_quiz/modal_quiz_question_bank', 'init', [
 115                  $thiscontext->id
 116              ]);
 117  
 118              $this->page->requires->js_call_amd('mod_quiz/modal_add_random_question', 'init', [
 119                  $thiscontext->id,
 120                  $pagevars['cat'],
 121                  $pageurl->out_as_local_url(true),
 122                  $pageurl->param('cmid'),
 123                  \core\plugininfo\qbank::is_plugin_enabled(\qbank_managecategories\helper::PLUGINNAME),
 124              ]);
 125  
 126              // Include the question chooser.
 127              $output .= $this->question_chooser();
 128          }
 129  
 130          return $output;
 131      }
 132  
 133      /**
 134       * Render any warnings that might be required about the state of the quiz,
 135       * e.g. if it has been attempted, or if the shuffle questions option is
 136       * turned on.
 137       *
 138       * @param structure $structure the quiz structure.
 139       * @return string HTML to output.
 140       */
 141      public function quiz_state_warnings(structure $structure) {
 142          $warnings = $structure->get_edit_page_warnings();
 143  
 144          if (empty($warnings)) {
 145              return '';
 146          }
 147  
 148          $output = [];
 149          foreach ($warnings as $warning) {
 150              $output[] = \html_writer::tag('p', $warning);
 151          }
 152          return $this->box(implode("\n", $output), 'statusdisplay');
 153      }
 154  
 155      /**
 156       * Render the status bar.
 157       *
 158       * @param structure $structure the quiz structure.
 159       * @return string HTML to output.
 160       */
 161      public function quiz_information(structure $structure) {
 162          list($currentstatus, $explanation) = $structure->get_dates_summary();
 163  
 164          $output = html_writer::span(
 165                      get_string('numquestionsx', 'quiz', $structure->get_question_count()),
 166                      'numberofquestions') . ' | ' .
 167                  html_writer::span($currentstatus, 'quizopeningstatus',
 168                      ['title' => $explanation]);
 169  
 170          return html_writer::div($output, 'statusbar');
 171      }
 172  
 173      /**
 174       * Render the form for setting a quiz' overall grade
 175       *
 176       * @param structure $structure the quiz structure.
 177       * @param \moodle_url $pageurl the canonical URL of this page.
 178       * @return string HTML to output.
 179       */
 180      public function maximum_grade_input($structure, \moodle_url $pageurl) {
 181          $output = '';
 182          $output .= html_writer::start_div('maxgrade');
 183          $output .= html_writer::start_tag('form', ['method' => 'post', 'action' => 'edit.php',
 184                  'class' => 'quizsavegradesform form-inline']);
 185          $output .= html_writer::start_tag('fieldset', ['class' => 'invisiblefieldset']);
 186          $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
 187          $output .= html_writer::input_hidden_params($pageurl);
 188          $output .= html_writer::tag('label', get_string('maximumgrade') . ' ',
 189                  ['for' => 'inputmaxgrade']);
 190          $output .= html_writer::empty_tag('input', ['type' => 'text', 'id' => 'inputmaxgrade',
 191                  'name' => 'maxgrade', 'size' => ($structure->get_decimal_places_for_grades() + 2),
 192                  'value' => $structure->formatted_quiz_grade(),
 193                  'class' => 'form-control']);
 194          $output .= html_writer::empty_tag('input', ['type' => 'submit', 'class' => 'btn btn-secondary ml-1',
 195                  'name' => 'savechanges', 'value' => get_string('save', 'quiz')]);
 196          $output .= html_writer::end_tag('fieldset');
 197          $output .= html_writer::end_tag('form');
 198          $output .= html_writer::end_tag('div');
 199          return $output;
 200      }
 201  
 202      /**
 203       * Return the repaginate button
 204       * @param structure $structure the structure of the quiz being edited.
 205       * @param \moodle_url $pageurl the canonical URL of this page.
 206       * @return string HTML to output.
 207       */
 208      protected function repaginate_button(structure $structure, \moodle_url $pageurl) {
 209          $header = html_writer::tag('span', get_string('repaginatecommand', 'quiz'), ['class' => 'repaginatecommand']);
 210          $form = $this->repaginate_form($structure, $pageurl);
 211  
 212          $buttonoptions = [
 213              'type'  => 'submit',
 214              'name'  => 'repaginate',
 215              'id'    => 'repaginatecommand',
 216              'value' => get_string('repaginatecommand', 'quiz'),
 217              'class' => 'btn btn-secondary mr-1',
 218              'data-header' => $header,
 219              'data-form'   => $form,
 220          ];
 221          if (!$structure->can_be_repaginated()) {
 222              $buttonoptions['disabled'] = 'disabled';
 223          } else {
 224              $this->page->requires->js_call_amd('mod_quiz/repaginate', 'init');
 225          }
 226  
 227          return html_writer::empty_tag('input', $buttonoptions);
 228      }
 229  
 230      /**
 231       * Generate the bulk action button.
 232       *
 233       * @param structure $structure the structure of the quiz being edited.
 234       * @return string HTML to output.
 235       */
 236      protected function selectmultiple_button(structure $structure) {
 237          $buttonoptions = [
 238              'type'  => 'button',
 239              'name'  => 'selectmultiple',
 240              'id'    => 'selectmultiplecommand',
 241              'value' => get_string('selectmultipleitems', 'quiz'),
 242              'class' => 'btn btn-secondary'
 243          ];
 244          if (!$structure->can_be_edited()) {
 245              $buttonoptions['disabled'] = 'disabled';
 246          }
 247  
 248          return html_writer::tag('button', get_string('selectmultipleitems', 'quiz'), $buttonoptions);
 249      }
 250  
 251      /**
 252       * Generate the controls that appear when the bulk action button is pressed.
 253       *
 254       * @param structure $structure the structure of the quiz being edited.
 255       * @return string HTML to output.
 256       */
 257      protected function selectmultiple_controls(structure $structure) {
 258          $output = '';
 259  
 260          // Bulk action button delete and bulk action button cancel.
 261          $buttondeleteoptions = [
 262              'type' => 'button',
 263              'id' => 'selectmultipledeletecommand',
 264              'value' => get_string('deleteselected', 'mod_quiz'),
 265              'class' => 'btn btn-secondary',
 266              'data-action' => 'toggle',
 267              'data-togglegroup' => $this->togglegroup,
 268              'data-toggle' => 'action',
 269              'disabled' => true
 270          ];
 271          $buttoncanceloptions = [
 272              'type' => 'button',
 273              'id' => 'selectmultiplecancelcommand',
 274              'value' => get_string('cancel', 'moodle'),
 275              'class' => 'btn btn-secondary'
 276          ];
 277  
 278          $groupoptions = [
 279              'class' => 'btn-group selectmultiplecommand actions m-1',
 280              'role' => 'group'
 281          ];
 282  
 283          $output .= html_writer::tag('div',
 284                          html_writer::tag('button', get_string('deleteselected', 'mod_quiz'), $buttondeleteoptions) .
 285                          " " .
 286                          html_writer::tag('button', get_string('cancel', 'moodle'),
 287                  $buttoncanceloptions), $groupoptions);
 288  
 289          $toolbaroptions = [
 290              'class' => 'btn-toolbar m-1',
 291              'role' => 'toolbar',
 292              'aria-label' => get_string('selectmultipletoolbar', 'quiz'),
 293          ];
 294  
 295          // Select all/deselect all questions.
 296          $selectallid = 'questionselectall';
 297          $selectalltext = get_string('selectall', 'moodle');
 298          $deselectalltext = get_string('deselectall', 'moodle');
 299          $mastercheckbox = new \core\output\checkbox_toggleall($this->togglegroup, true, [
 300              'id' => $selectallid,
 301              'name' => $selectallid,
 302              'value' => 1,
 303              'label' => $selectalltext,
 304              'selectall' => $selectalltext,
 305              'deselectall' => $deselectalltext,
 306          ], true);
 307  
 308          $selectdeselect = html_writer::div($this->render($mastercheckbox), 'selectmultiplecommandbuttons');
 309          $output .= html_writer::tag('div', $selectdeselect, $toolbaroptions);
 310          return $output;
 311      }
 312  
 313      /**
 314       * Return the repaginate form
 315       * @param structure $structure the structure of the quiz being edited.
 316       * @param \moodle_url $pageurl the canonical URL of this page.
 317       * @return string HTML to output.
 318       */
 319      protected function repaginate_form(structure $structure, \moodle_url $pageurl) {
 320          $perpage = [];
 321          $perpage[0] = get_string('allinone', 'quiz');
 322          for ($i = 1; $i <= 50; ++$i) {
 323              $perpage[$i] = $i;
 324          }
 325  
 326          $hiddenurl = clone($pageurl);
 327          $hiddenurl->param('sesskey', sesskey());
 328  
 329          $select = html_writer::select($perpage, 'questionsperpage',
 330                  $structure->get_questions_per_page(), false, ['class' => 'custom-select']);
 331  
 332          $buttonattributes = [
 333              'type' => 'submit',
 334              'name' => 'repaginate',
 335              'value' => get_string('go'),
 336              'class' => 'btn btn-secondary ml-1'
 337          ];
 338  
 339          $formcontent = html_writer::tag('form', html_writer::div(
 340                      html_writer::input_hidden_params($hiddenurl) .
 341                      get_string('repaginate', 'quiz', $select) .
 342                      html_writer::empty_tag('input', $buttonattributes)
 343                  ), ['action' => 'edit.php', 'method' => 'post']);
 344  
 345          return html_writer::div($formcontent, '', ['id' => 'repaginatedialog']);
 346      }
 347  
 348      /**
 349       * Render the total marks available for the quiz.
 350       *
 351       * @param \stdClass $quiz the quiz settings from the database.
 352       * @return string HTML to output.
 353       */
 354      public function total_marks($quiz) {
 355          $totalmark = html_writer::span(quiz_format_grade($quiz, $quiz->sumgrades), 'mod_quiz_summarks');
 356          return html_writer::tag('span',
 357                  get_string('totalmarksx', 'quiz', $totalmark),
 358                  ['class' => 'totalpoints']);
 359      }
 360  
 361      /**
 362       * Generate the starting container html for the start of a list of sections
 363       * @param structure $structure the structure of the quiz being edited.
 364       * @return string HTML to output.
 365       */
 366      protected function start_section_list(structure $structure) {
 367          $class = 'slots';
 368          if ($structure->get_section_count() == 1) {
 369              $class .= ' only-one-section';
 370          }
 371          return html_writer::start_tag('ul', ['class' => $class, 'role' => 'presentation']);
 372      }
 373  
 374      /**
 375       * Generate the closing container html for the end of a list of sections
 376       * @return string HTML to output.
 377       */
 378      protected function end_section_list() {
 379          return html_writer::end_tag('ul');
 380      }
 381  
 382      /**
 383       * Display the start of a section, before the questions.
 384       *
 385       * @param structure $structure the structure of the quiz being edited.
 386       * @param \stdClass $section The quiz_section entry from DB
 387       * @return string HTML to output.
 388       */
 389      protected function start_section($structure, $section) {
 390  
 391          $output = '';
 392  
 393          $sectionstyle = '';
 394          if ($structure->is_only_one_slot_in_section($section)) {
 395              $sectionstyle .= ' only-has-one-slot';
 396          }
 397          if ($section->shufflequestions) {
 398              $sectionstyle .= ' shuffled';
 399          }
 400  
 401          if ($section->heading) {
 402              $sectionheadingtext = format_string($section->heading);
 403              $sectionheading = html_writer::span($sectionheadingtext, 'instancesection');
 404          } else {
 405              // Use a sr-only default section heading, so we don't end up with an empty section heading.
 406              $sectionheadingtext = get_string('sectionnoname', 'quiz');
 407              $sectionheading = html_writer::span($sectionheadingtext, 'instancesection sr-only');
 408          }
 409  
 410          $output .= html_writer::start_tag('li', ['id' => 'section-'.$section->id,
 411              'class' => 'section main clearfix'.$sectionstyle, 'role' => 'presentation',
 412              'data-sectionname' => $sectionheadingtext]);
 413  
 414          $output .= html_writer::start_div('content');
 415  
 416          $output .= html_writer::start_div('section-heading');
 417  
 418          $headingtext = $this->heading(html_writer::span($sectionheading, 'sectioninstance'), 3);
 419  
 420          if (!$structure->can_be_edited()) {
 421              $editsectionheadingicon = '';
 422          } else {
 423              $editsectionheadingicon = html_writer::link(new \moodle_url('#'),
 424                  $this->pix_icon('t/editstring', get_string('sectionheadingedit', 'quiz', $sectionheadingtext),
 425                          'moodle', ['class' => 'editicon visibleifjs']),
 426                          ['class' => 'editing_section', 'data-action' => 'edit_section_title', 'role' => 'button']);
 427          }
 428          $output .= html_writer::div($headingtext . $editsectionheadingicon, 'instancesectioncontainer');
 429  
 430          if (!$structure->is_first_section($section) && $structure->can_be_edited()) {
 431              $output .= $this->section_remove_icon($section);
 432          }
 433          $output .= $this->section_shuffle_questions($structure, $section);
 434  
 435          $output .= html_writer::end_div($output, 'section-heading');
 436  
 437          return $output;
 438      }
 439  
 440      /**
 441       * Display a checkbox for shuffling question within a section.
 442       *
 443       * @param structure $structure object containing the structure of the quiz.
 444       * @param \stdClass $section data from the quiz_section table.
 445       * @return string HTML to output.
 446       */
 447      public function section_shuffle_questions(structure $structure, $section) {
 448          $checkboxattributes = [
 449              'type' => 'checkbox',
 450              'id' => 'shuffle-' . $section->id,
 451              'value' => 1,
 452              'data-action' => 'shuffle_questions',
 453              'class' => 'cm-edit-action',
 454          ];
 455  
 456          if (!$structure->can_be_edited()) {
 457              $checkboxattributes['disabled'] = 'disabled';
 458          }
 459          if ($section->shufflequestions) {
 460              $checkboxattributes['checked'] = 'checked';
 461          }
 462  
 463          if ($structure->is_first_section($section)) {
 464              $help = $this->help_icon('shufflequestions', 'quiz');
 465          } else {
 466              $help = '';
 467          }
 468  
 469          $helpspan = html_writer::span($help, 'shuffle-help-tip');
 470          $progressspan = html_writer::span('', 'shuffle-progress');
 471          $checkbox = html_writer::empty_tag('input', $checkboxattributes);
 472          $label = html_writer::label(get_string('shufflequestions', 'quiz'),
 473                  $checkboxattributes['id'], false);
 474          return html_writer::span($progressspan . $checkbox . $label. ' ' . $helpspan,
 475                  'instanceshufflequestions', ['data-action' => 'shuffle_questions']);
 476      }
 477  
 478      /**
 479       * Display the end of a section, after the questions.
 480       *
 481       * @return string HTML to output.
 482       */
 483      protected function end_section() {
 484          $output = html_writer::end_tag('div');
 485          $output .= html_writer::end_tag('li');
 486  
 487          return $output;
 488      }
 489  
 490      /**
 491       * Render an icon to remove a section from the quiz.
 492       *
 493       * @param stdClass $section the section to be removed.
 494       * @return string HTML to output.
 495       */
 496      public function section_remove_icon($section) {
 497          $title = get_string('sectionheadingremove', 'quiz', format_string($section->heading));
 498          $url = new \moodle_url('/mod/quiz/edit.php',
 499                  ['sesskey' => sesskey(), 'removesection' => '1', 'sectionid' => $section->id]);
 500          $image = $this->pix_icon('t/delete', $title);
 501          return $this->action_link($url, $image, null, [
 502                  'class' => 'cm-edit-action editing_delete', 'data-action' => 'deletesection']);
 503      }
 504  
 505      /**
 506       * Renders HTML to display the questions in a section of the quiz.
 507       *
 508       * This function calls {@link core_course_renderer::quiz_section_question()}
 509       *
 510       * @param structure $structure object containing the structure of the quiz.
 511       * @param \stdClass $section information about the section.
 512       * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
 513       * @param array $pagevars the variables from {@link \question_edit_setup()}.
 514       * @param \moodle_url $pageurl the canonical URL of this page.
 515       * @return string HTML to output.
 516       */
 517      public function questions_in_section(structure $structure, $section,
 518              $contexts, $pagevars, $pageurl) {
 519  
 520          $output = '';
 521          foreach ($structure->get_slots_in_section($section->id) as $slot) {
 522              $output .= $this->question_row($structure, $slot, $contexts, $pagevars, $pageurl);
 523          }
 524          return html_writer::tag('ul', $output, ['class' => 'section img-text']);
 525      }
 526  
 527      /**
 528       * Displays one question with the surrounding controls.
 529       *
 530       * @param structure $structure object containing the structure of the quiz.
 531       * @param int $slot which slot we are outputting.
 532       * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
 533       * @param array $pagevars the variables from {@link \question_edit_setup()}.
 534       * @param \moodle_url $pageurl the canonical URL of this page.
 535       * @return string HTML to output.
 536       */
 537      public function question_row(structure $structure, $slot, $contexts, $pagevars, $pageurl) {
 538          $output = '';
 539  
 540          $output .= $this->page_row($structure, $slot, $contexts, $pagevars, $pageurl);
 541  
 542          // Page split/join icon.
 543          $joinhtml = '';
 544          if ($structure->can_be_edited() && !$structure->is_last_slot_in_quiz($slot) &&
 545                                              !$structure->is_last_slot_in_section($slot)) {
 546              $joinhtml = $this->page_split_join_button($structure, $slot);
 547          }
 548          // Question HTML.
 549          $questionhtml = $this->question($structure, $slot, $pageurl);
 550          $qtype = $structure->get_question_type_for_slot($slot);
 551          $questionclasses = 'activity ' . $qtype . ' qtype_' . $qtype . ' slot';
 552  
 553          $output .= html_writer::tag('li', $questionhtml . $joinhtml,
 554                  ['class' => $questionclasses, 'id' => 'slot-' . $structure->get_slot_id_for_slot($slot),
 555                          'data-canfinish' => $structure->can_finish_during_the_attempt($slot)]);
 556  
 557          return $output;
 558      }
 559  
 560      /**
 561       * Displays one question with the surrounding controls.
 562       *
 563       * @param structure $structure object containing the structure of the quiz.
 564       * @param int $slot the first slot on the page we are outputting.
 565       * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
 566       * @param array $pagevars the variables from {@link \question_edit_setup()}.
 567       * @param \moodle_url $pageurl the canonical URL of this page.
 568       * @return string HTML to output.
 569       */
 570      public function page_row(structure $structure, $slot, $contexts, $pagevars, $pageurl) {
 571          $output = '';
 572  
 573          $pagenumber = $structure->get_page_number_for_slot($slot);
 574  
 575          // Put page in a heading for accessibility and styling.
 576          $page = $this->heading(get_string('page') . ' ' . $pagenumber, 4);
 577  
 578          if ($structure->is_first_slot_on_page($slot)) {
 579              // Add the add-menu at the page level.
 580              $addmenu = html_writer::tag('span', $this->add_menu_actions($structure,
 581                      $pagenumber, $pageurl, $contexts, $pagevars),
 582                      ['class' => 'add-menu-outer']);
 583  
 584              $addquestionform = $this->add_question_form($structure,
 585                      $pagenumber, $pageurl, $pagevars);
 586  
 587              $output .= html_writer::tag('li', $page . $addmenu . $addquestionform,
 588                      ['class' => 'pagenumber activity yui3-dd-drop page', 'id' => 'page-' . $pagenumber]);
 589          }
 590  
 591          return $output;
 592      }
 593  
 594      /**
 595       * Returns the add menu that is output once per page.
 596       * @param structure $structure object containing the structure of the quiz.
 597       * @param int $page the page number that this menu will add to.
 598       * @param \moodle_url $pageurl the canonical URL of this page.
 599       * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
 600       * @param array $pagevars the variables from {@link \question_edit_setup()}.
 601       * @return string HTML to output.
 602       */
 603      public function add_menu_actions(structure $structure, $page, \moodle_url $pageurl,
 604              \core_question\local\bank\question_edit_contexts $contexts, array $pagevars) {
 605  
 606          $actions = $this->edit_menu_actions($structure, $page, $pageurl, $pagevars);
 607          if (empty($actions)) {
 608              return '';
 609          }
 610          $menu = new \action_menu();
 611          $trigger = html_writer::tag('span', get_string('add', 'quiz'), ['class' => 'add-menu']);
 612          $menu->set_menu_trigger($trigger);
 613          // The menu appears within an absolutely positioned element causing width problems.
 614          // Make sure no-wrap is set so that we don't get a squashed menu.
 615          $menu->set_nowrap_on_items(true);
 616  
 617          // Disable the link if quiz has attempts.
 618          if (!$structure->can_be_edited()) {
 619              return $this->render($menu);
 620          }
 621  
 622          foreach ($actions as $action) {
 623              if ($action instanceof \action_menu_link) {
 624                  $action->add_class('add-menu');
 625              }
 626              $menu->add($action);
 627          }
 628          $menu->attributes['class'] .= ' page-add-actions commands';
 629  
 630          // Prioritise the menu ahead of all other actions.
 631          $menu->prioritise = true;
 632  
 633          return $this->render($menu);
 634      }
 635  
 636      /**
 637       * Returns the list of actions to go in the add menu.
 638       * @param structure $structure object containing the structure of the quiz.
 639       * @param int $page the page number that this menu will add to.
 640       * @param \moodle_url $pageurl the canonical URL of this page.
 641       * @param array $pagevars the variables from {@link \question_edit_setup()}.
 642       * @return array the actions.
 643       */
 644      public function edit_menu_actions(structure $structure, $page,
 645              \moodle_url $pageurl, array $pagevars) {
 646          $questioncategoryid = question_get_category_id_from_pagevars($pagevars);
 647          static $str;
 648          if (!isset($str)) {
 649              $str = get_strings(['addasection', 'addaquestion', 'addarandomquestion',
 650                      'addarandomselectedquestion', 'questionbank'], 'quiz');
 651          }
 652  
 653          // Get section, page, slotnumber and maxmark.
 654          $actions = [];
 655  
 656          // Add a new question to the quiz.
 657          $returnurl = new \moodle_url($pageurl, ['addonpage' => $page]);
 658          $params = ['returnurl' => $returnurl->out_as_local_url(false),
 659                  'cmid' => $structure->get_cmid(), 'category' => $questioncategoryid,
 660                  'addonpage' => $page, 'appendqnumstring' => 'addquestion'];
 661  
 662          $actions['addaquestion'] = new \action_menu_link_secondary(
 663              new \moodle_url('/question/bank/editquestion/addquestion.php', $params),
 664              new \pix_icon('t/add', $str->addaquestion, 'moodle', ['class' => 'iconsmall', 'title' => '']),
 665              $str->addaquestion, ['class' => 'cm-edit-action addquestion', 'data-action' => 'addquestion']
 666          );
 667  
 668          // Call question bank.
 669          $icon = new \pix_icon('t/add', $str->questionbank, 'moodle', ['class' => 'iconsmall', 'title' => '']);
 670          if ($page) {
 671              $title = get_string('addquestionfrombanktopage', 'quiz', $page);
 672          } else {
 673              $title = get_string('addquestionfrombankatend', 'quiz');
 674          }
 675          $attributes = ['class' => 'cm-edit-action questionbank',
 676                  'data-header' => $title, 'data-action' => 'questionbank', 'data-addonpage' => $page];
 677          $actions['questionbank'] = new \action_menu_link_secondary($pageurl, $icon, $str->questionbank, $attributes);
 678  
 679          // Add a random question.
 680          if ($structure->can_add_random_questions()) {
 681              $returnurl = new \moodle_url('/mod/quiz/edit.php', ['cmid' => $structure->get_cmid(), 'data-addonpage' => $page]);
 682              $params = ['returnurl' => $returnurl, 'cmid' => $structure->get_cmid(), 'appendqnumstring' => 'addarandomquestion'];
 683              $url = new \moodle_url('/mod/quiz/edit.php', $params);
 684              $icon = new \pix_icon('t/add', $str->addarandomquestion, 'moodle', ['class' => 'iconsmall', 'title' => '']);
 685              $attributes = ['class' => 'cm-edit-action addarandomquestion', 'data-action' => 'addarandomquestion'];
 686              if ($page) {
 687                  $title = get_string('addrandomquestiontopage', 'quiz', $page);
 688              } else {
 689                  $title = get_string('addrandomquestionatend', 'quiz');
 690              }
 691              $attributes = array_merge(['data-header' => $title, 'data-addonpage' => $page], $attributes);
 692              $actions['addarandomquestion'] = new \action_menu_link_secondary($url, $icon, $str->addarandomquestion, $attributes);
 693          }
 694  
 695          // Add a new section to the add_menu if possible. This is always added to the HTML
 696          // then hidden with CSS when no needed, so that as things are re-ordered, etc. with
 697          // Ajax it can be relevaled again when necessary.
 698          $params = ['cmid' => $structure->get_cmid(), 'addsectionatpage' => $page];
 699  
 700          $actions['addasection'] = new \action_menu_link_secondary(
 701              new \moodle_url($pageurl, $params),
 702              new \pix_icon('t/add', $str->addasection, 'moodle', ['class' => 'iconsmall', 'title' => '']),
 703              $str->addasection, ['class' => 'cm-edit-action addasection', 'data-action' => 'addasection']
 704          );
 705  
 706          return $actions;
 707      }
 708  
 709      /**
 710       * Render the form that contains the data for adding a new question to the quiz.
 711       *
 712       * @param structure $structure object containing the structure of the quiz.
 713       * @param int $page the page number that this menu will add to.
 714       * @param \moodle_url $pageurl the canonical URL of this page.
 715       * @param array $pagevars the variables from {@link \question_edit_setup()}.
 716       * @return string HTML to output.
 717       */
 718      protected function add_question_form(structure $structure, $page, \moodle_url $pageurl, array $pagevars) {
 719  
 720          $questioncategoryid = question_get_category_id_from_pagevars($pagevars);
 721  
 722          $output = html_writer::tag('input', null,
 723                  ['type' => 'hidden', 'name' => 'returnurl',
 724                          'value' => $pageurl->out_as_local_url(false, ['addonpage' => $page])]);
 725          $output .= html_writer::tag('input', null,
 726                  ['type' => 'hidden', 'name' => 'cmid', 'value' => $structure->get_cmid()]);
 727          $output .= html_writer::tag('input', null,
 728                  ['type' => 'hidden', 'name' => 'appendqnumstring', 'value' => 'addquestion']);
 729          $output .= html_writer::tag('input', null,
 730                  ['type' => 'hidden', 'name' => 'category', 'value' => $questioncategoryid]);
 731  
 732          return html_writer::tag('form', html_writer::div($output),
 733                  ['class' => 'addnewquestion', 'method' => 'post',
 734                          'action' => new \moodle_url('/question/bank/editquestion/addquestion.php')]);
 735      }
 736  
 737      /**
 738       * Display a question.
 739       *
 740       * @param structure $structure object containing the structure of the quiz.
 741       * @param int $slot the first slot on the page we are outputting.
 742       * @param \moodle_url $pageurl the canonical URL of this page.
 743       * @return string HTML to output.
 744       */
 745      public function question(structure $structure, int $slot, \moodle_url $pageurl) {
 746          // Get the data required by the question_slot template.
 747          $slotid = $structure->get_slot_id_for_slot($slot);
 748  
 749          $output = '';
 750          $output .= html_writer::start_tag('div');
 751  
 752          if ($structure->can_be_edited()) {
 753              $output .= $this->question_move_icon($structure, $slot);
 754          }
 755  
 756          if ($structure->can_display_number_be_customised($slot)) {
 757              $questionnumber = $this->output->render($structure->make_slot_display_number_in_place_editable(
 758                      $slotid, $structure->get_context()));
 759          } else {
 760              $questionnumber = $structure->get_displayed_number_for_slot($slot);
 761          }
 762  
 763          $data = [
 764              'slotid' => $slotid,
 765              'canbeedited' => $structure->can_be_edited(),
 766              'checkbox' => $this->get_checkbox_render($structure, $slot),
 767              'questionnumber' => $this->question_number($questionnumber, $structure->get_slot_by_number($slot)->defaultnumber),
 768              'questionname' => $this->get_question_name_for_slot($structure, $slot, $pageurl),
 769              'questionicons' => $this->get_action_icon($structure, $slot, $pageurl),
 770              'questiondependencyicon' => ($structure->can_be_edited() ? $this->question_dependency_icon($structure, $slot) : ''),
 771              'versionselection' => false,
 772              'draftversion' => $structure->get_question_in_slot($slot)->status == question_version_status::QUESTION_STATUS_DRAFT,
 773          ];
 774  
 775          $data['versionoptions'] = [];
 776          if ($structure->get_slot_by_number($slot)->qtype !== 'random') {
 777              $data['versionselection'] = true;
 778              $data['versionoption'] = $structure->get_version_choices_for_slot($slot);
 779              $this->page->requires->js_call_amd('mod_quiz/question_slot', 'init', [$slotid]);
 780          }
 781  
 782          // Render the question slot template.
 783          $output .= $this->render_from_template('mod_quiz/question_slot', $data);
 784  
 785          $output .= html_writer::end_tag('div');
 786  
 787          return $output;
 788      }
 789  
 790      /**
 791       * Get the checkbox render.
 792       *
 793       * @param structure $structure object containing the structure of the quiz.
 794       * @param int $slot the slot on the page we are outputting.
 795       * @return string HTML to output.
 796       */
 797      public function get_checkbox_render(structure $structure, int $slot) : string {
 798          $questionslot = $structure->get_displayed_number_for_slot($slot);
 799          $checkbox = new \core\output\checkbox_toggleall($this->togglegroup, false,
 800              [
 801                  'id' => 'selectquestion-' . $slot,
 802                  'name' => 'selectquestion[]',
 803                  'classes' => 'select-multiple-checkbox',
 804                  'label' => get_string('selectquestionslot', 'quiz', $questionslot),
 805                  'labelclasses' => 'sr-only',
 806              ]);
 807  
 808          return $this->render($checkbox);
 809      }
 810  
 811      /**
 812       * Get the question name for the slot.
 813       *
 814       * @param structure $structure object containing the structure of the quiz.
 815       * @param int $slot the slot on the page we are outputting.
 816       * @param \moodle_url $pageurl the canonical URL of this page.
 817       * @return string HTML to output.
 818       */
 819      public function get_question_name_for_slot(structure $structure, int $slot, \moodle_url $pageurl) : string {
 820          // Display the link to the question (or do nothing if question has no url).
 821          if ($structure->get_question_type_for_slot($slot) === 'random') {
 822              $questionname = $this->random_question($structure, $slot, $pageurl);
 823          } else {
 824              $questionname = $this->question_name($structure, $slot, $pageurl);
 825          }
 826  
 827          return $questionname;
 828      }
 829  
 830      /**
 831       * Get the action icons render.
 832       *
 833       * @param structure $structure object containing the structure of the quiz.
 834       * @param int $slot the slot on the page we are outputting.
 835       * @param \moodle_url $pageurl the canonical URL of this page.
 836       * @return string HTML to output.
 837       */
 838      public function get_action_icon(structure $structure, int $slot, \moodle_url $pageurl) : string {
 839          // Action icons.
 840          $qtype = $structure->get_question_type_for_slot($slot);
 841          $slotinfo = $structure->get_slot_by_number($slot);
 842          $questionicons = '';
 843          if ($qtype !== 'random') {
 844              $questionicons .= $this->question_preview_icon($structure->get_quiz(),
 845                      $structure->get_question_in_slot($slot),
 846                      null, null, $slotinfo->requestedversion ?: question_preview_options::ALWAYS_LATEST);
 847          }
 848          if ($structure->can_be_edited() && $structure->has_use_capability($slot)) {
 849              $questionicons .= $this->question_remove_icon($structure, $slot, $pageurl);
 850          }
 851          $questionicons .= $this->marked_out_of_field($structure, $slot);
 852  
 853          return $questionicons;
 854      }
 855  
 856      /**
 857       * Render the move icon.
 858       *
 859       * @param structure $structure object containing the structure of the quiz.
 860       * @param int $slot the first slot on the page we are outputting.
 861       * @return string The markup for the move action.
 862       */
 863      public function question_move_icon(structure $structure, $slot) {
 864          return html_writer::link(new \moodle_url('#'),
 865              $this->pix_icon('i/dragdrop', get_string('move'), 'moodle', ['class' => 'iconsmall', 'title' => '']),
 866              ['class' => 'editing_move', 'data-action' => 'move']
 867          );
 868      }
 869  
 870      /**
 871       * Output the question number.
 872       *
 873       * @param string $editablenumber The, which may be an in-place editable.
 874       * @param string $uncustomisednumber The un-customised number number, or 'i'.
 875       * @return string HTML to output.
 876       */
 877      public function question_number(string $editablenumber, string $uncustomisednumber) {
 878          if ($editablenumber !== get_string('infoshort', 'quiz')) {
 879              $editablenumber = html_writer::span(get_string('question'), 'accesshide') . ' ' . $editablenumber;
 880              $uncustomisednumber = html_writer::span(get_string('question'), 'accesshide') . ' ' . $uncustomisednumber;
 881          }
 882          return html_writer::tag('span', $editablenumber, ['class' => 'slotnumber unshuffled']) .
 883                  html_writer::tag('span', $uncustomisednumber, ['class' => 'slotnumber shuffled']);
 884      }
 885  
 886      /**
 887       * Render the preview icon.
 888       *
 889       * @param \stdClass $quiz the quiz settings from the database.
 890       * @param \stdClass $questiondata which question to preview.
 891       *      If ->questionid is set, that is used instead of ->id.
 892       * @param bool $label if true, show the preview question label after the icon
 893       * @param int $variant which question variant to preview (optional).
 894       * @param int $restartversion version to use when restarting the preview
 895       * @return string HTML to output.
 896       */
 897      public function question_preview_icon($quiz, $questiondata, $label = null, $variant = null, $restartversion = null) {
 898          $question = clone($questiondata);
 899          if (isset($question->questionid)) {
 900  
 901              $question->id = $question->questionid;
 902          }
 903  
 904          $url = quiz_question_preview_url($quiz, $question, $variant, $restartversion);
 905  
 906          // Do we want a label?
 907          $strpreviewlabel = '';
 908          if ($label) {
 909              $strpreviewlabel = ' ' . get_string('preview', 'quiz');
 910          }
 911  
 912          // Build the icon.
 913          $strpreviewquestion = get_string('previewquestion', 'quiz');
 914          $image = $this->pix_icon('t/preview', $strpreviewquestion);
 915  
 916          $action = new \popup_action('click', $url, 'questionpreview',
 917                  \qbank_previewquestion\helper::question_preview_popup_params());
 918  
 919          return $this->action_link($url, $image . $strpreviewlabel, $action,
 920                  ['title' => $strpreviewquestion, 'class' => 'preview']);
 921      }
 922  
 923      /**
 924       * Render an icon to remove a question from the quiz.
 925       *
 926       * @param structure $structure object containing the structure of the quiz.
 927       * @param int $slot the first slot on the page we are outputting.
 928       * @param \moodle_url $pageurl the canonical URL of the edit page.
 929       * @return string HTML to output.
 930       */
 931      public function question_remove_icon(structure $structure, $slot, $pageurl) {
 932          $url = new \moodle_url($pageurl, ['sesskey' => sesskey(), 'remove' => $slot]);
 933          $strdelete = get_string('delete');
 934  
 935          $image = $this->pix_icon('t/delete', $strdelete);
 936  
 937          return $this->action_link($url, $image, null, ['title' => $strdelete,
 938                      'class' => 'cm-edit-action editing_delete', 'data-action' => 'delete']);
 939      }
 940  
 941      /**
 942       * Display an icon to split or join two pages of the quiz.
 943       *
 944       * @param structure $structure object containing the structure of the quiz.
 945       * @param int $slot the first slot on the page we are outputting.
 946       * @return string HTML to output.
 947       */
 948      public function page_split_join_button($structure, $slot) {
 949          $insertpagebreak = !$structure->is_last_slot_on_page($slot);
 950          $url = new \moodle_url('repaginate.php', ['quizid' => $structure->get_quizid(),
 951                  'slot' => $slot, 'repag' => $insertpagebreak ? 2 : 1, 'sesskey' => sesskey()]);
 952  
 953          if ($insertpagebreak) {
 954              $title = get_string('addpagebreak', 'quiz');
 955              $image = $this->image_icon('e/insert_page_break', $title);
 956              $action = 'addpagebreak';
 957          } else {
 958              $title = get_string('removepagebreak', 'quiz');
 959              $image = $this->image_icon('e/remove_page_break', $title);
 960              $action = 'removepagebreak';
 961          }
 962  
 963          // Disable the link if quiz has attempts.
 964          $disabled = null;
 965          if (!$structure->can_be_edited()) {
 966              $disabled = 'disabled';
 967          }
 968          return html_writer::span($this->action_link($url, $image, null, ['title' => $title,
 969                      'class' => 'page_split_join cm-edit-action', 'disabled' => $disabled, 'data-action' => $action]),
 970                  'page_split_join_wrapper');
 971      }
 972  
 973      /**
 974       * Display the icon for whether this question can only be seen if the previous
 975       * one has been answered.
 976       *
 977       * @param structure $structure object containing the structure of the quiz.
 978       * @param int $slot the first slot on the page we are outputting.
 979       * @return string HTML to output.
 980       */
 981      public function question_dependency_icon($structure, $slot) {
 982          $a = [
 983              'thisq' => $structure->get_displayed_number_for_slot($slot),
 984              'previousq' => $structure->get_displayed_number_for_slot(max($slot - 1, 1)),
 985          ];
 986          if ($structure->is_question_dependent_on_previous_slot($slot)) {
 987              $title = get_string('questiondependencyremove', 'quiz', $a);
 988              $image = $this->pix_icon('t/locked', get_string('questiondependsonprevious', 'quiz'),
 989                      'moodle', ['title' => '']);
 990              $action = 'removedependency';
 991          } else {
 992              $title = get_string('questiondependencyadd', 'quiz', $a);
 993              $image = $this->pix_icon('t/unlocked', get_string('questiondependencyfree', 'quiz'),
 994                      'moodle', ['title' => '']);
 995              $action = 'adddependency';
 996          }
 997  
 998          // Disable the link if quiz has attempts.
 999          $disabled = null;
1000          if (!$structure->can_be_edited()) {
1001              $disabled = 'disabled';
1002          }
1003          $extraclass = '';
1004          if (!$structure->can_question_depend_on_previous_slot($slot)) {
1005              $extraclass = ' question_dependency_cannot_depend';
1006          }
1007          return html_writer::span($this->action_link('#', $image, null, ['title' => $title,
1008                  'class' => 'cm-edit-action', 'disabled' => $disabled, 'data-action' => $action]),
1009                  'question_dependency_wrapper' . $extraclass);
1010      }
1011  
1012      /**
1013       * Renders html to display a name with the link to the question on a quiz edit page
1014       *
1015       * If the user does not have permission to edi the question, it is rendered
1016       * without a link
1017       *
1018       * @param structure $structure object containing the structure of the quiz.
1019       * @param int $slot which slot we are outputting.
1020       * @param \moodle_url $pageurl the canonical URL of this page.
1021       * @return string HTML to output.
1022       */
1023      public function question_name(structure $structure, $slot, $pageurl) {
1024          $output = '';
1025  
1026          $question = $structure->get_question_in_slot($slot);
1027          $editurl = new \moodle_url('/question/bank/editquestion/question.php', [
1028                  'returnurl' => $pageurl->out_as_local_url(),
1029                  'cmid' => $structure->get_cmid(), 'id' => $question->questionid]);
1030  
1031          $instancename = quiz_question_tostring($question);
1032  
1033          $qtype = \question_bank::get_qtype($question->qtype, false);
1034          $namestr = $qtype->local_name();
1035  
1036          $icon = $this->pix_icon('icon', $namestr, $qtype->plugin_name(), ['title' => $namestr,
1037                  'class' => 'activityicon', 'alt' => ' ', 'role' => 'presentation']);
1038  
1039          $editicon = $this->pix_icon('t/edit', '', 'moodle', ['title' => '']);
1040  
1041          // Need plain question name without html tags for link title.
1042          $title = shorten_text(format_string($question->name), 100);
1043  
1044          // Display the link itself.
1045          $activitylink = $icon . html_writer::tag('span', $editicon . $instancename, ['class' => 'instancename']);
1046          $output .= html_writer::link($editurl, $activitylink,
1047                  ['title' => get_string('editquestion', 'quiz').' '.$title]);
1048  
1049          return $output;
1050      }
1051  
1052      /**
1053       * Renders html to display a random question the link to edit the configuration
1054       * and also to see that category in the question bank.
1055       *
1056       * @param structure $structure object containing the structure of the quiz.
1057       * @param int $slotnumber which slot we are outputting.
1058       * @param \moodle_url $pageurl the canonical URL of this page.
1059       * @return string HTML to output.
1060       */
1061      public function random_question(structure $structure, $slotnumber, $pageurl) {
1062          $question = $structure->get_question_in_slot($slotnumber);
1063          $slot = $structure->get_slot_by_number($slotnumber);
1064          $editurl = new \moodle_url('/mod/quiz/editrandom.php',
1065                  ['returnurl' => $pageurl->out_as_local_url(), 'slotid' => $slot->id]);
1066  
1067          $temp = clone($question);
1068          $temp->questiontext = '';
1069          $temp->name = qbank_helper::describe_random_question($slot);
1070          $instancename = quiz_question_tostring($temp);
1071  
1072          $configuretitle = get_string('configurerandomquestion', 'quiz');
1073          $qtype = \question_bank::get_qtype($question->qtype, false);
1074          $namestr = $qtype->local_name();
1075          $icon = $this->pix_icon('icon', $namestr, $qtype->plugin_name(), ['title' => $namestr,
1076                  'class' => 'icon activityicon', 'alt' => ' ', 'role' => 'presentation']);
1077  
1078          $editicon = $this->pix_icon('t/edit', $configuretitle, 'moodle', ['title' => '']);
1079          $qbankurlparams = [
1080              'cmid' => $structure->get_cmid(),
1081              'cat' => $slot->category . ',' . $slot->contextid,
1082          ];
1083  
1084          $slottags = [];
1085          if (isset($slot->randomtags)) {
1086              $slottags = $slot->randomtags;
1087          }
1088          foreach ($slottags as $index => $slottag) {
1089              $slottag = explode(',', $slottag);
1090              $qbankurlparams["qtagids[{$index}]"] = $slottag[0];
1091          }
1092  
1093          // If this is a random question, display a link to show the questions
1094          // selected from in the question bank.
1095          $qbankurl = new \moodle_url('/question/edit.php', $qbankurlparams);
1096          $qbanklink = ' ' . \html_writer::link($qbankurl,
1097                          get_string('seequestions', 'quiz'), ['class' => 'mod_quiz_random_qbank_link']);
1098  
1099          return html_writer::link($editurl, $icon . $editicon, ['title' => $configuretitle]) .
1100                  ' ' . $instancename . ' ' . $qbanklink;
1101      }
1102  
1103      /**
1104       * Display the 'marked out of' information for a question.
1105       * Along with the regrade action.
1106       * @param structure $structure object containing the structure of the quiz.
1107       * @param int $slot which slot we are outputting.
1108       * @return string HTML to output.
1109       */
1110      public function marked_out_of_field(structure $structure, $slot) {
1111          if (!$structure->is_real_question($slot)) {
1112              $output = html_writer::span('',
1113                      'instancemaxmark decimalplaces_' . $structure->get_decimal_places_for_question_marks());
1114  
1115              $output .= html_writer::span(
1116                      $this->pix_icon('spacer', '', 'moodle', ['class' => 'editicon visibleifjs', 'title' => '']),
1117                      'editing_maxmark');
1118              return html_writer::span($output, 'instancemaxmarkcontainer infoitem');
1119          }
1120  
1121          $output = html_writer::span($structure->formatted_question_grade($slot),
1122                  'instancemaxmark decimalplaces_' . $structure->get_decimal_places_for_question_marks(),
1123                  ['title' => get_string('maxmark', 'quiz')]);
1124  
1125          $output .= html_writer::span(
1126              html_writer::link(
1127                  new \moodle_url('#'),
1128                  $this->pix_icon('t/editstring', '', 'moodle', ['class' => 'editicon visibleifjs', 'title' => '']),
1129                  [
1130                      'class' => 'editing_maxmark',
1131                      'data-action' => 'editmaxmark',
1132                      'title' => get_string('editmaxmark', 'quiz'),
1133                  ]
1134              )
1135          );
1136          return html_writer::span($output, 'instancemaxmarkcontainer');
1137      }
1138  
1139      /**
1140       * Renders the question chooser.
1141       *
1142       * @param renderable
1143       * @return string
1144       */
1145      public function render_question_chooser(renderable $chooser) {
1146          return $this->render_from_template('mod_quiz/question_chooser', $chooser->export_for_template($this));
1147      }
1148  
1149      /**
1150       * Render the question type chooser dialogue.
1151       * @return string HTML to output.
1152       */
1153      public function question_chooser() {
1154          $chooser = \mod_quiz\output\question_chooser::get($this->page->course, [], null);
1155          $container = html_writer::div($this->render($chooser), '', ['id' => 'qtypechoicecontainer']);
1156          return html_writer::div($container, 'createnewquestion');
1157      }
1158  
1159      /**
1160       * Render the contents of the question bank pop-up in its initial state,
1161       * when it just contains a loading progress indicator.
1162       * @return string HTML to output.
1163       */
1164      public function question_bank_loading() {
1165          return html_writer::div($this->pix_icon('i/loading', get_string('loading')), 'questionbankloading');
1166      }
1167  
1168      /**
1169       * Initialise the JavaScript for the general editing. (JavaScript for popups
1170       * is handled with the specific code for those.)
1171       *
1172       * @param structure $structure object containing the structure of the quiz.
1173       * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
1174       * @param array $pagevars the variables from {@link \question_edit_setup()}.
1175       * @param \moodle_url $pageurl the canonical URL of this page.
1176       * @return bool Always returns true
1177       */
1178      protected function initialise_editing_javascript(structure $structure,
1179              \core_question\local\bank\question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
1180  
1181          $config = new \stdClass();
1182          $config->resourceurl = '/mod/quiz/edit_rest.php';
1183          $config->sectionurl = '/mod/quiz/edit_rest.php';
1184          $config->pageparams = [];
1185          $config->questiondecimalpoints = $structure->get_decimal_places_for_question_marks();
1186          $config->pagehtml = $this->new_page_template($structure, $contexts, $pagevars, $pageurl);
1187          $config->addpageiconhtml = $this->add_page_icon_template($structure);
1188  
1189          $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
1190                  'M.mod_quiz.init_resource_toolbox',
1191                  [[
1192                          'courseid' => $structure->get_courseid(),
1193                          'quizid' => $structure->get_quizid(),
1194                          'ajaxurl' => $config->resourceurl,
1195                          'config' => $config,
1196                  ]]
1197          );
1198          unset($config->pagehtml);
1199          unset($config->addpageiconhtml);
1200  
1201          $this->page->requires->strings_for_js(['areyousureremoveselected'], 'quiz');
1202          $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
1203                  'M.mod_quiz.init_section_toolbox',
1204                  [[
1205                          'courseid' => $structure,
1206                          'quizid' => $structure->get_quizid(),
1207                          'ajaxurl' => $config->sectionurl,
1208                          'config' => $config,
1209                  ]]
1210          );
1211  
1212          $this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_section_dragdrop',
1213                  [[
1214                          'courseid' => $structure,
1215                          'quizid' => $structure->get_quizid(),
1216                          'ajaxurl' => $config->sectionurl,
1217                          'config' => $config,
1218                  ]], null, true);
1219  
1220          $this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_resource_dragdrop',
1221                  [[
1222                          'courseid' => $structure,
1223                          'quizid' => $structure->get_quizid(),
1224                          'ajaxurl' => $config->resourceurl,
1225                          'config' => $config,
1226                  ]], null, true);
1227  
1228          // Require various strings for the command toolbox.
1229          $this->page->requires->strings_for_js([
1230                  'clicktohideshow',
1231                  'deletechecktype',
1232                  'deletechecktypename',
1233                  'edittitle',
1234                  'edittitleinstructions',
1235                  'emptydragdropregion',
1236                  'hide',
1237                  'markedthistopic',
1238                  'markthistopic',
1239                  'move',
1240                  'movecontent',
1241                  'moveleft',
1242                  'movesection',
1243                  'page',
1244                  'question',
1245                  'selectall',
1246                  'show',
1247                  'tocontent',
1248          ], 'moodle');
1249  
1250          $this->page->requires->strings_for_js([
1251                  'addpagebreak',
1252                  'cannotremoveallsectionslots',
1253                  'cannotremoveslots',
1254                  'confirmremovesectionheading',
1255                  'confirmremovequestion',
1256                  'dragtoafter',
1257                  'dragtostart',
1258                  'numquestionsx',
1259                  'sectionheadingedit',
1260                  'sectionheadingremove',
1261                  'sectionnoname',
1262                  'removepagebreak',
1263                  'questiondependencyadd',
1264                  'questiondependencyfree',
1265                  'questiondependencyremove',
1266                  'questiondependsonprevious',
1267          ], 'quiz');
1268  
1269          foreach (\question_bank::get_all_qtypes() as $qtype => $notused) {
1270              $this->page->requires->string_for_js('pluginname', 'qtype_' . $qtype);
1271          }
1272  
1273          return true;
1274      }
1275  
1276      /**
1277       * HTML for a page, with ids stripped, so it can be used as a javascript template.
1278       *
1279       * @param structure $structure object containing the structure of the quiz.
1280       * @param \core_question\local\bank\question_edit_contexts $contexts the relevant question bank contexts.
1281       * @param array $pagevars the variables from {@link \question_edit_setup()}.
1282       * @param \moodle_url $pageurl the canonical URL of this page.
1283       * @return string HTML for a new page.
1284       */
1285      protected function new_page_template(structure $structure,
1286              \core_question\local\bank\question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
1287          if (!$structure->has_questions()) {
1288              return '';
1289          }
1290  
1291          $pagehtml = $this->page_row($structure, 1, $contexts, $pagevars, $pageurl);
1292  
1293          // Normalise the page number.
1294          $pagenumber = $structure->get_page_number_for_slot(1);
1295          $strcontexts = [];
1296          $strcontexts[] = 'page-';
1297          $strcontexts[] = get_string('page') . ' ';
1298          $strcontexts[] = 'addonpage%3D';
1299          $strcontexts[] = 'addonpage=';
1300          $strcontexts[] = 'addonpage="';
1301          $strcontexts[] = get_string('addquestionfrombanktopage', 'quiz', '');
1302          $strcontexts[] = 'data-addonpage%3D';
1303          $strcontexts[] = 'action-menu-';
1304  
1305          foreach ($strcontexts as $strcontext) {
1306              $pagehtml = str_replace($strcontext . $pagenumber, $strcontext . '%%PAGENUMBER%%', $pagehtml);
1307          }
1308  
1309          return $pagehtml;
1310      }
1311  
1312      /**
1313       * HTML for a page, with ids stripped, so it can be used as a javascript template.
1314       *
1315       * @param structure $structure object containing the structure of the quiz.
1316       * @return string HTML for a new icon
1317       */
1318      protected function add_page_icon_template(structure $structure) {
1319  
1320          if (!$structure->has_questions()) {
1321              return '';
1322          }
1323  
1324          $html = $this->page_split_join_button($structure, 1);
1325          return str_replace('&amp;slot=1&amp;', '&amp;slot=%%SLOT%%&amp;', $html);
1326      }
1327  
1328      /**
1329       * Return the contents of the question bank, to be displayed in the question-bank pop-up.
1330       *
1331       * @param \mod_quiz\question\bank\custom_view $questionbank the question bank view object.
1332       * @param array $pagevars the variables from {@link \question_edit_setup()}.
1333       * @return string HTML to output / send back in response to an AJAX request.
1334       */
1335      public function question_bank_contents(\mod_quiz\question\bank\custom_view $questionbank, array $pagevars) {
1336  
1337          $qbank = $questionbank->render($pagevars, 'editq');
1338          return html_writer::div(html_writer::div($qbank, 'bd'), 'questionbankformforpopup');
1339      }
1340  }