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 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   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   * mod_lesson data generator.
  19   *
  20   * @package    mod_lesson
  21   * @category   test
  22   * @copyright  2013 Marina Glancy
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->dirroot.'/mod/lesson/locallib.php');
  29  
  30  /**
  31   * mod_lesson data generator class.
  32   *
  33   * @package    mod_lesson
  34   * @category   test
  35   * @copyright  2013 Marina Glancy
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class mod_lesson_generator extends testing_module_generator {
  39  
  40      /**
  41       * @var int keep track of how many pages have been created.
  42       */
  43      protected $pagecount = 0;
  44  
  45      /**
  46       * @var array list of candidate pages to be created when all answers have been added.
  47       */
  48      protected $candidatepages = [];
  49  
  50      /**
  51       * @var array map of readable jumpto to integer value.
  52       */
  53      protected $jumptomap = [
  54          'This page' => LESSON_THISPAGE,
  55          'Next page' => LESSON_NEXTPAGE,
  56          'Previous page' => LESSON_PREVIOUSPAGE,
  57          'End of lesson' => LESSON_EOL,
  58          'Unseen question within a content page' => LESSON_UNSEENBRANCHPAGE,
  59          'Random question within a content page' => LESSON_RANDOMPAGE,
  60          'Random content page' => LESSON_RANDOMBRANCH,
  61          'Unseen question within a cluster' => LESSON_CLUSTERJUMP,
  62      ];
  63  
  64      /**
  65       * To be called from data reset code only,
  66       * do not use in tests.
  67       * @return void
  68       */
  69      public function reset() {
  70          $this->pagecount = 0;
  71          $this->candidatepages = [];
  72          parent::reset();
  73      }
  74  
  75      /**
  76       * Creates a lesson instance for testing purposes.
  77       *
  78       * @param null|array|stdClass $record data for module being generated.
  79       * @param null|array $options general options for course module.
  80       * @return stdClass record from module-defined table with additional field cmid (corresponding id in course_modules table)
  81       */
  82      public function create_instance($record = null, array $options = null) {
  83          global $CFG;
  84  
  85          // Add default values for lesson.
  86          $lessonconfig = get_config('mod_lesson');
  87          $record = (array)$record + array(
  88              'progressbar' => $lessonconfig->progressbar,
  89              'ongoing' => $lessonconfig->ongoing,
  90              'displayleft' => $lessonconfig->displayleftmenu,
  91              'displayleftif' => $lessonconfig->displayleftif,
  92              'slideshow' => $lessonconfig->slideshow,
  93              'maxanswers' => $lessonconfig->maxanswers,
  94              'feedback' => $lessonconfig->defaultfeedback,
  95              'activitylink' => 0,
  96              'available' => 0,
  97              'deadline' => 0,
  98              'usepassword' => 0,
  99              'password' => '',
 100              'dependency' => 0,
 101              'timespent' => 0,
 102              'completed' => 0,
 103              'gradebetterthan' => 0,
 104              'modattempts' => $lessonconfig->modattempts,
 105              'review' => $lessonconfig->displayreview,
 106              'maxattempts' => $lessonconfig->maximumnumberofattempts,
 107              'nextpagedefault' => $lessonconfig->defaultnextpage,
 108              'maxpages' => $lessonconfig->numberofpagestoshow,
 109              'practice' => $lessonconfig->practice,
 110              'custom' => $lessonconfig->customscoring,
 111              'retake' => $lessonconfig->retakesallowed,
 112              'usemaxgrade' => $lessonconfig->handlingofretakes,
 113              'minquestions' => $lessonconfig->minimumnumberofquestions,
 114              'grade' => 100,
 115          );
 116          if (!isset($record['mediafile'])) {
 117              require_once($CFG->libdir.'/filelib.php');
 118              $record['mediafile'] = file_get_unused_draft_itemid();
 119          }
 120  
 121          return parent::create_instance($record, (array)$options);
 122      }
 123  
 124      /**
 125       * Creates a page for testing purposes. The page will be created when answers are added.
 126       *
 127       * @param null|array|stdClass $record data for page being generated.
 128       * @param null|array $options general options.
 129       */
 130      public function create_page($record = null, array $options = null) {
 131          $record = (array) $record;
 132  
 133          // Pages require answers to work. Add it as a candidate page to be created once answers have been added.
 134          $record['answer_editor'] = [];
 135          $record['response_editor'] = [];
 136          $record['jumpto'] = [];
 137          $record['score'] = [];
 138  
 139          if (!isset($record['previouspage']) || $record['previouspage'] === '') {
 140              // Previous page not set, set it to the last candidate page (if any).
 141              $record['previouspage'] = empty($this->candidatepages) ? '0' : end($this->candidatepages)['title'];
 142          }
 143  
 144          $this->candidatepages[] = $record;
 145      }
 146  
 147      /**
 148       * Creates a page and its answers for testing purposes.
 149       *
 150       * @param array $record data for page being generated.
 151       * @return stdClass created page, null if couldn't be created because it has a jump to a page that doesn't exist.
 152       * @throws coding_exception
 153       */
 154      private function perform_create_page(array $record): ?stdClass {
 155          global $DB;
 156  
 157          $lesson = $DB->get_record('lesson', ['id' => $record['lessonid']], '*', MUST_EXIST);
 158          $cm = get_coursemodule_from_instance('lesson', $lesson->id);
 159          $lesson->cmid = $cm->id;
 160          $qtype = $record['qtype'];
 161  
 162          unset($record['qtype']);
 163          unset($record['lessonid']);
 164  
 165          if (isset($record['content'])) {
 166              $record['contents_editor'] = [
 167                  'text' => $record['content'],
 168                  'format' => FORMAT_MOODLE,
 169                  'itemid' => 0,
 170              ];
 171              unset($record['content']);
 172          }
 173  
 174          $record['pageid'] = $this->get_previouspage_id($lesson->id, $record['previouspage']);
 175          unset($record['previouspage']);
 176  
 177          try {
 178              $record['jumpto'] = $this->convert_page_jumpto($lesson->id, $record['jumpto']);
 179          } catch (coding_exception $e) {
 180              // This page has a jump to a page that hasn't been created yet.
 181              return null;
 182          }
 183  
 184          switch ($qtype) {
 185              case 'content':
 186              case 'cluster':
 187              case 'endofcluster':
 188              case 'endofbranch':
 189                  $funcname = "create_{$qtype}";
 190                  break;
 191              default:
 192                  $funcname = "create_question_{$qtype}";
 193          }
 194  
 195          if (!method_exists($this, $funcname)) {
 196              throw new coding_exception('The page '.$record['title']." has an invalid qtype: $qtype");
 197          }
 198  
 199          return $this->{$funcname}($lesson, $record);
 200      }
 201  
 202      /**
 203       * Creates a content page for testing purposes.
 204       *
 205       * @param stdClass $lesson instance where to create the page.
 206       * @param array|stdClass $record data for page being generated.
 207       * @return stdClass page record.
 208       */
 209      public function create_content($lesson, $record = array()) {
 210          global $DB, $CFG;
 211          $now = time();
 212          $this->pagecount++;
 213          $record = (array)$record + array(
 214              'lessonid' => $lesson->id,
 215              'title' => 'Lesson page '.$this->pagecount,
 216              'timecreated' => $now,
 217              'qtype' => 20, // LESSON_PAGE_BRANCHTABLE
 218              'pageid' => 0, // By default insert in the beginning.
 219          );
 220          if (!isset($record['contents_editor'])) {
 221              $record['contents_editor'] = array(
 222                  'text' => 'Contents of lesson page '.$this->pagecount,
 223                  'format' => FORMAT_MOODLE,
 224                  'itemid' => 0,
 225              );
 226          }
 227          $context = context_module::instance($lesson->cmid);
 228          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 229          return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
 230      }
 231  
 232      /**
 233       * Create True/false question pages.
 234       * @param object $lesson
 235       * @param array $record
 236       * @return stdClass page record.
 237       */
 238      public function create_question_truefalse($lesson, $record = array()) {
 239          global $DB, $CFG;
 240          $now = time();
 241          $this->pagecount++;
 242          $record = (array)$record + array(
 243              'lessonid' => $lesson->id,
 244              'title' => 'Lesson TF question '.$this->pagecount,
 245              'timecreated' => $now,
 246              'qtype' => 2,  // LESSON_PAGE_TRUEFALSE.
 247              'pageid' => 0, // By default insert in the beginning.
 248          );
 249          if (!isset($record['contents_editor'])) {
 250              $record['contents_editor'] = array(
 251                  'text' => 'The answer is TRUE '.$this->pagecount,
 252                  'format' => FORMAT_HTML,
 253                  'itemid' => 0
 254              );
 255          }
 256  
 257          // First Answer (TRUE).
 258          if (!isset($record['answer_editor'][0])) {
 259              $record['answer_editor'][0] = array(
 260                  'text' => 'TRUE answer for '.$this->pagecount,
 261                  'format' => FORMAT_HTML
 262              );
 263          }
 264          if (!isset($record['jumpto'][0])) {
 265              $record['jumpto'][0] = LESSON_NEXTPAGE;
 266          }
 267  
 268          // Second Answer (FALSE).
 269          if (!isset($record['answer_editor'][1])) {
 270              $record['answer_editor'][1] = array(
 271                  'text' => 'FALSE answer for '.$this->pagecount,
 272                  'format' => FORMAT_HTML
 273              );
 274          }
 275          if (!isset($record['jumpto'][1])) {
 276              $record['jumpto'][1] = LESSON_THISPAGE;
 277          }
 278  
 279          $context = context_module::instance($lesson->cmid);
 280          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 281          return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
 282      }
 283  
 284      /**
 285       * Create multichoice question pages.
 286       * @param object $lesson
 287       * @param array $record
 288       * @return stdClass page record.
 289       */
 290      public function create_question_multichoice($lesson, $record = array()) {
 291          global $DB, $CFG;
 292          $now = time();
 293          $this->pagecount++;
 294          $record = (array)$record + array(
 295              'lessonid' => $lesson->id,
 296              'title' => 'Lesson multichoice question '.$this->pagecount,
 297              'timecreated' => $now,
 298              'qtype' => 3,  // LESSON_PAGE_MULTICHOICE.
 299              'pageid' => 0, // By default insert in the beginning.
 300          );
 301          if (!isset($record['contents_editor'])) {
 302              $record['contents_editor'] = array(
 303                  'text' => 'Pick the correct answer '.$this->pagecount,
 304                  'format' => FORMAT_HTML,
 305                  'itemid' => 0
 306              );
 307          }
 308  
 309          // First Answer (correct).
 310          if (!isset($record['answer_editor'][0])) {
 311              $record['answer_editor'][0] = array(
 312                  'text' => 'correct answer for '.$this->pagecount,
 313                  'format' => FORMAT_HTML
 314              );
 315          }
 316          if (!isset($record['jumpto'][0])) {
 317              $record['jumpto'][0] = LESSON_NEXTPAGE;
 318          }
 319  
 320          // Second Answer (incorrect).
 321          if (!isset($record['answer_editor'][1])) {
 322              $record['answer_editor'][1] = array(
 323                  'text' => 'correct answer for '.$this->pagecount,
 324                  'format' => FORMAT_HTML
 325              );
 326          }
 327          if (!isset($record['jumpto'][1])) {
 328              $record['jumpto'][1] = LESSON_THISPAGE;
 329          }
 330  
 331          $context = context_module::instance($lesson->cmid);
 332          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 333          return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
 334      }
 335  
 336      /**
 337       * Create essay question pages.
 338       * @param object $lesson
 339       * @param array $record
 340       * @return stdClass page record.
 341       */
 342      public function create_question_essay($lesson, $record = array()) {
 343          global $DB, $CFG;
 344          $now = time();
 345          $this->pagecount++;
 346          $record = (array)$record + array(
 347              'lessonid' => $lesson->id,
 348              'title' => 'Lesson Essay question '.$this->pagecount,
 349              'timecreated' => $now,
 350              'qtype' => 10, // LESSON_PAGE_ESSAY.
 351              'pageid' => 0, // By default insert in the beginning.
 352          );
 353          if (!isset($record['contents_editor'])) {
 354              $record['contents_editor'] = array(
 355                  'text' => 'Write an Essay '.$this->pagecount,
 356                  'format' => FORMAT_HTML,
 357                  'itemid' => 0
 358              );
 359          }
 360  
 361          // Essays have an answer of NULL.
 362          if (!isset($record['answer_editor'][0])) {
 363              $record['answer_editor'][0] = array(
 364                  'text' => null,
 365                  'format' => FORMAT_MOODLE
 366              );
 367          }
 368          if (!isset($record['jumpto'][0])) {
 369              $record['jumpto'][0] = LESSON_NEXTPAGE;
 370          }
 371  
 372          $context = context_module::instance($lesson->cmid);
 373          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 374          return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
 375      }
 376  
 377      /**
 378       * Create matching question pages.
 379       * @param object $lesson
 380       * @param array $record
 381       * @return stdClass page record.
 382       */
 383      public function create_question_matching($lesson, $record = array()) {
 384          global $DB, $CFG;
 385          $now = time();
 386          $this->pagecount++;
 387          $record = (array)$record + array(
 388              'lessonid' => $lesson->id,
 389              'title' => 'Lesson Matching question '.$this->pagecount,
 390              'timecreated' => $now,
 391              'qtype' => 5,  // LESSON_PAGE_MATCHING.
 392              'pageid' => 0, // By default insert in the beginning.
 393          );
 394          if (!isset($record['contents_editor'])) {
 395              $record['contents_editor'] = array(
 396                  'text' => 'Match the values '.$this->pagecount,
 397                  'format' => FORMAT_HTML,
 398                  'itemid' => 0
 399              );
 400          }
 401          // Feedback for correct result.
 402          if (!isset($record['answer_editor'][0])) {
 403              $record['answer_editor'][0] = array(
 404                  'text' => '',
 405                  'format' => FORMAT_HTML
 406              );
 407          }
 408          // Feedback for wrong result.
 409          if (!isset($record['answer_editor'][1])) {
 410              $record['answer_editor'][1] = array(
 411                  'text' => '',
 412                  'format' => FORMAT_HTML
 413              );
 414          }
 415          // First answer value.
 416          if (!isset($record['answer_editor'][2])) {
 417              $record['answer_editor'][2] = array(
 418                  'text' => 'Match value 1',
 419                  'format' => FORMAT_HTML
 420              );
 421          }
 422          // First response value.
 423          if (!isset($record['response_editor'][2])) {
 424              $record['response_editor'][2] = 'Match answer 1';
 425          }
 426          // Second Matching value.
 427          if (!isset($record['answer_editor'][3])) {
 428              $record['answer_editor'][3] = array(
 429                  'text' => 'Match value 2',
 430                  'format' => FORMAT_HTML
 431              );
 432          }
 433          // Second Matching answer.
 434          if (!isset($record['response_editor'][3])) {
 435              $record['response_editor'][3] = 'Match answer 2';
 436          }
 437  
 438          // Jump Values.
 439          if (!isset($record['jumpto'][0])) {
 440              $record['jumpto'][0] = LESSON_NEXTPAGE;
 441          }
 442          if (!isset($record['jumpto'][1])) {
 443              $record['jumpto'][1] = LESSON_THISPAGE;
 444          }
 445  
 446          // Mark the correct values.
 447          if (!isset($record['score'][0])) {
 448              $record['score'][0] = 1;
 449          }
 450          $context = context_module::instance($lesson->cmid);
 451          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 452          return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
 453      }
 454  
 455      /**
 456       * Create shortanswer question pages.
 457       * @param object $lesson
 458       * @param array $record
 459       * @return stdClass page record.
 460       */
 461      public function create_question_shortanswer($lesson, $record = array()) {
 462          global $DB, $CFG;
 463          $now = time();
 464          $this->pagecount++;
 465          $record = (array)$record + array(
 466              'lessonid' => $lesson->id,
 467              'title' => 'Lesson Shortanswer question '.$this->pagecount,
 468              'timecreated' => $now,
 469              'qtype' => 1,  // LESSON_PAGE_SHORTANSWER.
 470              'pageid' => 0, // By default insert in the beginning.
 471          );
 472          if (!isset($record['contents_editor'])) {
 473              $record['contents_editor'] = array(
 474                  'text' => 'Fill in the blank '.$this->pagecount,
 475                  'format' => FORMAT_HTML,
 476                  'itemid' => 0
 477              );
 478          }
 479  
 480          // First Answer (correct).
 481          if (!isset($record['answer_editor'][0])) {
 482              $record['answer_editor'][0] = array(
 483                  'text' => 'answer'.$this->pagecount,
 484                  'format' => FORMAT_MOODLE
 485              );
 486          }
 487          if (!isset($record['jumpto'][0])) {
 488              $record['jumpto'][0] = LESSON_NEXTPAGE;
 489          }
 490  
 491          $context = context_module::instance($lesson->cmid);
 492          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 493          return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
 494      }
 495  
 496      /**
 497       * Create shortanswer question pages.
 498       * @param object $lesson
 499       * @param array $record
 500       * @return stdClass page record.
 501       */
 502      public function create_question_numeric($lesson, $record = array()) {
 503          global $DB, $CFG;
 504          $now = time();
 505          $this->pagecount++;
 506          $record = (array)$record + array(
 507              'lessonid' => $lesson->id,
 508              'title' => 'Lesson numerical question '.$this->pagecount,
 509              'timecreated' => $now,
 510              'qtype' => 8,  // LESSON_PAGE_NUMERICAL.
 511              'pageid' => 0, // By default insert in the beginning.
 512          );
 513          if (!isset($record['contents_editor'])) {
 514              $record['contents_editor'] = array(
 515                  'text' => 'Numerical question '.$this->pagecount,
 516                  'format' => FORMAT_HTML,
 517                  'itemid' => 0
 518              );
 519          }
 520  
 521          // First Answer (correct).
 522          if (!isset($record['answer_editor'][0])) {
 523              $record['answer_editor'][0] = array(
 524                  'text' => $this->pagecount,
 525                  'format' => FORMAT_MOODLE
 526              );
 527          }
 528          if (!isset($record['jumpto'][0])) {
 529              $record['jumpto'][0] = LESSON_NEXTPAGE;
 530          }
 531  
 532          $context = context_module::instance($lesson->cmid);
 533          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 534          return $DB->get_record('lesson_pages', array('id' => $page->id), '*', MUST_EXIST);
 535      }
 536  
 537      /**
 538       * Creates a cluster page for testing purposes.
 539       *
 540       * @param stdClass $lesson instance where to create the page.
 541       * @param array $record data for page being generated.
 542       * @return stdClass page record.
 543       */
 544      public function create_cluster(stdClass $lesson, array $record = []): stdClass {
 545          global $DB, $CFG;
 546          $now = time();
 547          $this->pagecount++;
 548          $record = $record + [
 549              'lessonid' => $lesson->id,
 550              'title' => 'Cluster '.$this->pagecount,
 551              'timecreated' => $now,
 552              'qtype' => 30, // LESSON_PAGE_CLUSTER.
 553              'pageid' => 0, // By default insert in the beginning.
 554          ];
 555          if (!isset($record['contents_editor'])) {
 556              $record['contents_editor'] = [
 557                  'text' => 'Cluster '.$this->pagecount,
 558                  'format' => FORMAT_MOODLE,
 559                  'itemid' => 0,
 560              ];
 561          }
 562          $context = context_module::instance($lesson->cmid);
 563          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 564          return $DB->get_record('lesson_pages', ['id' => $page->id], '*', MUST_EXIST);
 565      }
 566  
 567      /**
 568       * Creates a end of cluster page for testing purposes.
 569       *
 570       * @param stdClass $lesson instance where to create the page.
 571       * @param array $record data for page being generated.
 572       * @return stdClass page record.
 573       */
 574      public function create_endofcluster(stdClass $lesson, array $record = []): stdClass {
 575          global $DB, $CFG;
 576          $now = time();
 577          $this->pagecount++;
 578          $record = $record + [
 579              'lessonid' => $lesson->id,
 580              'title' => 'End of cluster '.$this->pagecount,
 581              'timecreated' => $now,
 582              'qtype' => 31, // LESSON_PAGE_ENDOFCLUSTER.
 583              'pageid' => 0, // By default insert in the beginning.
 584          ];
 585          if (!isset($record['contents_editor'])) {
 586              $record['contents_editor'] = [
 587                  'text' => 'End of cluster '.$this->pagecount,
 588                  'format' => FORMAT_MOODLE,
 589                  'itemid' => 0,
 590              ];
 591          }
 592          $context = context_module::instance($lesson->cmid);
 593          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 594          return $DB->get_record('lesson_pages', ['id' => $page->id], '*', MUST_EXIST);
 595      }
 596  
 597      /**
 598       * Creates a end of branch page for testing purposes.
 599       *
 600       * @param stdClass $lesson instance where to create the page.
 601       * @param array $record data for page being generated.
 602       * @return stdClass page record.
 603       */
 604      public function create_endofbranch(stdClass $lesson, array $record = []): stdClass {
 605          global $DB, $CFG;
 606          $now = time();
 607          $this->pagecount++;
 608          $record = $record + [
 609              'lessonid' => $lesson->id,
 610              'title' => 'End of branch '.$this->pagecount,
 611              'timecreated' => $now,
 612              'qtype' => 21, // LESSON_PAGE_ENDOFBRANCH.
 613              'pageid' => 0, // By default insert in the beginning.
 614          ];
 615          if (!isset($record['contents_editor'])) {
 616              $record['contents_editor'] = [
 617                  'text' => 'End of branch '.$this->pagecount,
 618                  'format' => FORMAT_MOODLE,
 619                  'itemid' => 0,
 620              ];
 621          }
 622          $context = context_module::instance($lesson->cmid);
 623          $page = lesson_page::create((object)$record, new lesson($lesson), $context, $CFG->maxbytes);
 624          return $DB->get_record('lesson_pages', ['id' => $page->id], '*', MUST_EXIST);
 625      }
 626  
 627      /**
 628       * Create a lesson override (either user or group).
 629       *
 630       * @param array $data must specify lessonid, and one of userid or groupid.
 631       * @throws coding_exception
 632       */
 633      public function create_override(array $data): void {
 634          global $DB;
 635  
 636          if (!isset($data['lessonid'])) {
 637              throw new coding_exception('Must specify lessonid when creating a lesson override.');
 638          }
 639  
 640          if (!isset($data['userid']) && !isset($data['groupid'])) {
 641              throw new coding_exception('Must specify one of userid or groupid when creating a lesson override.');
 642          }
 643  
 644          if (isset($data['userid']) && isset($data['groupid'])) {
 645              throw new coding_exception('Cannot specify both userid and groupid when creating a lesson override.');
 646          }
 647  
 648          $DB->insert_record('lesson_overrides', (object) $data);
 649      }
 650  
 651      /**
 652       * Creates an answer in a page for testing purposes.
 653       *
 654       * @param null|array|stdClass $record data for module being generated.
 655       * @param null|array $options general options.
 656       * @throws coding_exception
 657       */
 658      public function create_answer($record = null, array $options = null) {
 659          $record = (array) $record;
 660  
 661          $candidatepage = null;
 662          $pagetitle = $record['page'];
 663          $found = false;
 664          foreach ($this->candidatepages as &$candidatepage) {
 665              if ($candidatepage['title'] === $pagetitle) {
 666                  $found = true;
 667                  break;
 668              }
 669          }
 670  
 671          if (!$found) {
 672              throw new coding_exception("Page '$pagetitle' not found in candidate pages. Please make sure the page exists "
 673                  . 'and all answers are in the same table.');
 674          }
 675  
 676          if (isset($record['answer'])) {
 677              $candidatepage['answer_editor'][] = [
 678                  'text' => $record['answer'],
 679                  'format' => FORMAT_HTML,
 680              ];
 681          } else {
 682              $candidatepage['answer_editor'][] = null;
 683          }
 684  
 685          if (isset($record['response'])) {
 686              $candidatepage['response_editor'][] = [
 687                  'text' => $record['response'],
 688                  'format' => FORMAT_HTML,
 689              ];
 690          } else {
 691              $candidatepage['response_editor'][] = null;
 692          }
 693  
 694          $candidatepage['jumpto'][] = $record['jumpto'] ?? LESSON_THISPAGE;
 695          $candidatepage['score'][] = $record['score'] ?? 0;
 696      }
 697  
 698      /**
 699       * All answers in a table have been generated, create the pages.
 700       */
 701      public function finish_generate_answer() {
 702          $this->create_candidate_pages();
 703      }
 704  
 705      /**
 706       * Create candidate pages.
 707       *
 708       * @throws coding_exception
 709       */
 710      protected function create_candidate_pages(): void {
 711          // For performance reasons it would be better to use a topological sort algorithm. But since test cases shouldn't have
 712          // a lot of paged and complex jumps it was implemented using a simpler approach.
 713          $consecutiveblocked = 0;
 714  
 715          while (count($this->candidatepages) > 0) {
 716              $page = array_shift($this->candidatepages);
 717              $id = $this->perform_create_page($page);
 718  
 719              if ($id === null) {
 720                  // Page cannot be created yet because of jumpto. Move it to the end of list.
 721                  $consecutiveblocked++;
 722                  $this->candidatepages[] = $page;
 723  
 724                  if ($consecutiveblocked === count($this->candidatepages)) {
 725                      throw new coding_exception('There is a circular dependency in pages jumps.');
 726                  }
 727              } else {
 728                  $consecutiveblocked = 0;
 729              }
 730          }
 731      }
 732  
 733      /**
 734       * Calculate the previous page id.
 735       * If no page title is supplied, use the last page created in the lesson (0 if no pages).
 736       * If page title is supplied, search it in DB and the list of candidate pages.
 737       *
 738       * @param int $lessonid the lesson id.
 739       * @param string $pagetitle the page title, for example 'Test page'. '0' if no previous page.
 740       * @return int corresponding id. 0 if no previous page.
 741       * @throws coding_exception
 742       */
 743      protected function get_previouspage_id(int $lessonid, string $pagetitle): int {
 744          global $DB;
 745  
 746          if (is_numeric($pagetitle) && intval($pagetitle) === 0) {
 747              return 0;
 748          }
 749  
 750          $pages = $DB->get_records('lesson_pages', ['lessonid' => $lessonid, 'title' => $pagetitle], 'id ASC', 'id, title');
 751  
 752          if (count($pages) > 1) {
 753              throw new coding_exception("More than one page with '$pagetitle' found");
 754          } else if (!empty($pages)) {
 755              return current($pages)->id;
 756          }
 757  
 758          // Page doesn't exist, search if it's a candidate page. If it is, use its previous page instead.
 759          foreach ($this->candidatepages as $candidatepage) {
 760              if ($candidatepage['title'] === $pagetitle) {
 761                  return $this->get_previouspage_id($lessonid, $candidatepage['previouspage']);
 762              }
 763          }
 764  
 765          throw new coding_exception("Page '$pagetitle' not found");
 766      }
 767  
 768      /**
 769       * Convert the jumpto using a string to an integer value.
 770       * The jumpto can contain a page name or one of our predefined values.
 771       *
 772       * @param int $lessonid the lesson id.
 773       * @param array|null $jumptolist list of jumpto to treat.
 774       * @return array|null list of jumpto already treated.
 775       * @throws coding_exception
 776       */
 777      protected function convert_page_jumpto(int $lessonid, ?array $jumptolist): ?array {
 778          global $DB;
 779  
 780          if (empty($jumptolist)) {
 781              return $jumptolist;
 782          }
 783  
 784          foreach ($jumptolist as $i => $jumpto) {
 785              if (empty($jumpto) || is_numeric($jumpto)) {
 786                  continue;
 787              }
 788  
 789              if (isset($this->jumptomap[$jumpto])) {
 790                  $jumptolist[$i] = $this->jumptomap[$jumpto];
 791  
 792                  continue;
 793              }
 794  
 795              $page = $DB->get_record('lesson_pages', ['lessonid' => $lessonid, 'title' => $jumpto], 'id');
 796              if ($page === false) {
 797                  throw new coding_exception("Jump '$jumpto' not found in pages.");
 798              }
 799  
 800              $jumptolist[$i] = $page->id;
 801          }
 802  
 803          return $jumptolist;
 804      }
 805  }