Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * Local library file for Lesson.  These are non-standard functions that are used
  20   * only by Lesson.
  21   *
  22   * @package mod_lesson
  23   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
  25   **/
  26  
  27  /** Make sure this isn't being directly accessed */
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /** Include the files that are required by this module */
  31  require_once($CFG->dirroot.'/course/moodleform_mod.php');
  32  require_once($CFG->dirroot . '/mod/lesson/lib.php');
  33  require_once($CFG->libdir . '/filelib.php');
  34  
  35  /** This page */
  36  define('LESSON_THISPAGE', 0);
  37  /** Next page -> any page not seen before */
  38  define("LESSON_UNSEENPAGE", 1);
  39  /** Next page -> any page not answered correctly */
  40  define("LESSON_UNANSWEREDPAGE", 2);
  41  /** Jump to Next Page */
  42  define("LESSON_NEXTPAGE", -1);
  43  /** End of Lesson */
  44  define("LESSON_EOL", -9);
  45  /** Jump to an unseen page within a branch and end of branch or end of lesson */
  46  define("LESSON_UNSEENBRANCHPAGE", -50);
  47  /** Jump to Previous Page */
  48  define("LESSON_PREVIOUSPAGE", -40);
  49  /** Jump to a random page within a branch and end of branch or end of lesson */
  50  define("LESSON_RANDOMPAGE", -60);
  51  /** Jump to a random Branch */
  52  define("LESSON_RANDOMBRANCH", -70);
  53  /** Cluster Jump */
  54  define("LESSON_CLUSTERJUMP", -80);
  55  /** Undefined */
  56  define("LESSON_UNDEFINED", -99);
  57  
  58  /** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
  59  define("LESSON_MAX_EVENT_LENGTH", "432000");
  60  
  61  /** Answer format is HTML */
  62  define("LESSON_ANSWER_HTML", "HTML");
  63  
  64  /** Placeholder answer for all other answers. */
  65  define("LESSON_OTHER_ANSWERS", "@#wronganswer#@");
  66  
  67  //////////////////////////////////////////////////////////////////////////////////////
  68  /// Any other lesson functions go here.  Each of them must have a name that
  69  /// starts with lesson_
  70  
  71  /**
  72   * Checks to see if a LESSON_CLUSTERJUMP or
  73   * a LESSON_UNSEENBRANCHPAGE is used in a lesson.
  74   *
  75   * This function is only executed when a teacher is
  76   * checking the navigation for a lesson.
  77   *
  78   * @param stdClass $lesson Id of the lesson that is to be checked.
  79   * @return boolean True or false.
  80   **/
  81  function lesson_display_teacher_warning($lesson) {
  82      global $DB;
  83  
  84      // get all of the lesson answers
  85      $params = array ("lessonid" => $lesson->id);
  86      if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) {
  87          // no answers, then not using cluster or unseen
  88          return false;
  89      }
  90      // just check for the first one that fulfills the requirements
  91      foreach ($lessonanswers as $lessonanswer) {
  92          if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) {
  93              return true;
  94          }
  95      }
  96  
  97      // if no answers use either of the two jumps
  98      return false;
  99  }
 100  
 101  /**
 102   * Interprets the LESSON_UNSEENBRANCHPAGE jump.
 103   *
 104   * will return the pageid of a random unseen page that is within a branch
 105   *
 106   * @param lesson $lesson
 107   * @param int $userid Id of the user.
 108   * @param int $pageid Id of the page from which we are jumping.
 109   * @return int Id of the next page.
 110   **/
 111  function lesson_unseen_question_jump($lesson, $user, $pageid) {
 112      global $DB;
 113  
 114      // get the number of retakes
 115      if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) {
 116          $retakes = 0;
 117      }
 118  
 119      // get all the lesson_attempts aka what the user has seen
 120      if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) {
 121          foreach($viewedpages as $viewed) {
 122              $seenpages[] = $viewed->pageid;
 123          }
 124      } else {
 125          $seenpages = array();
 126      }
 127  
 128      // get the lesson pages
 129      $lessonpages = $lesson->load_all_pages();
 130  
 131      if ($pageid == LESSON_UNSEENBRANCHPAGE) {  // this only happens when a student leaves in the middle of an unseen question within a branch series
 132          $pageid = $seenpages[0];  // just change the pageid to the last page viewed inside the branch table
 133      }
 134  
 135      // go up the pages till branch table
 136      while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
 137          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 138              break;
 139          }
 140          $pageid = $lessonpages[$pageid]->prevpageid;
 141      }
 142  
 143      $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
 144  
 145      // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array
 146      $unseen = array();
 147      foreach($pagesinbranch as $page) {
 148          if (!in_array($page->id, $seenpages)) {
 149              $unseen[] = $page->id;
 150          }
 151      }
 152  
 153      if(count($unseen) == 0) {
 154          if(isset($pagesinbranch)) {
 155              $temp = end($pagesinbranch);
 156              $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL
 157          } else {
 158              // there are no pages inside the branch, so return the next page
 159              $nextpage = $lessonpages[$pageid]->nextpageid;
 160          }
 161          if ($nextpage == 0) {
 162              return LESSON_EOL;
 163          } else {
 164              return $nextpage;
 165          }
 166      } else {
 167          return $unseen[rand(0, count($unseen)-1)];  // returns a random page id for the next page
 168      }
 169  }
 170  
 171  /**
 172   * Handles the unseen branch table jump.
 173   *
 174   * @param lesson $lesson
 175   * @param int $userid User id.
 176   * @return int Will return the page id of a branch table or end of lesson
 177   **/
 178  function lesson_unseen_branch_jump($lesson, $userid) {
 179      global $DB;
 180  
 181      if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) {
 182          $retakes = 0;
 183      }
 184  
 185      if (!$seenbranches = $lesson->get_content_pages_viewed($retakes, $userid, 'timeseen DESC')) {
 186          print_error('cannotfindrecords', 'lesson');
 187      }
 188  
 189      // get the lesson pages
 190      $lessonpages = $lesson->load_all_pages();
 191  
 192      // this loads all the viewed branch tables into $seen until it finds the branch table with the flag
 193      // which is the branch table that starts the unseenbranch function
 194      $seen = array();
 195      foreach ($seenbranches as $seenbranch) {
 196          if (!$seenbranch->flag) {
 197              $seen[$seenbranch->pageid] = $seenbranch->pageid;
 198          } else {
 199              $start = $seenbranch->pageid;
 200              break;
 201          }
 202      }
 203      // this function searches through the lesson pages to find all the branch tables
 204      // that follow the flagged branch table
 205      $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table
 206      $branchtables = array();
 207      while ($pageid != 0) {  // grab all of the branch table till eol
 208          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 209              $branchtables[] = $lessonpages[$pageid]->id;
 210          }
 211          $pageid = $lessonpages[$pageid]->nextpageid;
 212      }
 213      $unseen = array();
 214      foreach ($branchtables as $branchtable) {
 215          // load all of the unseen branch tables into unseen
 216          if (!array_key_exists($branchtable, $seen)) {
 217              $unseen[] = $branchtable;
 218          }
 219      }
 220      if (count($unseen) > 0) {
 221          return $unseen[rand(0, count($unseen)-1)];  // returns a random page id for the next page
 222      } else {
 223          return LESSON_EOL;  // has viewed all of the branch tables
 224      }
 225  }
 226  
 227  /**
 228   * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE).
 229   *
 230   * @param lesson $lesson
 231   * @param int $pageid The id of the page that we are jumping from (?)
 232   * @return int The pageid of a random page that is within a branch table
 233   **/
 234  function lesson_random_question_jump($lesson, $pageid) {
 235      global $DB;
 236  
 237      // get the lesson pages
 238      $params = array ("lessonid" => $lesson->id);
 239      if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) {
 240          print_error('cannotfindpages', 'lesson');
 241      }
 242  
 243      // go up the pages till branch table
 244      while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
 245  
 246          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 247              break;
 248          }
 249          $pageid = $lessonpages[$pageid]->prevpageid;
 250      }
 251  
 252      // get the pages within the branch
 253      $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
 254  
 255      if(count($pagesinbranch) == 0) {
 256          // there are no pages inside the branch, so return the next page
 257          return $lessonpages[$pageid]->nextpageid;
 258      } else {
 259          return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id;  // returns a random page id for the next page
 260      }
 261  }
 262  
 263  /**
 264   * Calculates a user's grade for a lesson.
 265   *
 266   * @param object $lesson The lesson that the user is taking.
 267   * @param int $retries The attempt number.
 268   * @param int $userid Id of the user (optional, default current user).
 269   * @return object { nquestions => number of questions answered
 270                      attempts => number of question attempts
 271                      total => max points possible
 272                      earned => points earned by student
 273                      grade => calculated percentage grade
 274                      nmanual => number of manually graded questions
 275                      manualpoints => point value for manually graded questions }
 276   */
 277  function lesson_grade($lesson, $ntries, $userid = 0) {
 278      global $USER, $DB;
 279  
 280      if (empty($userid)) {
 281          $userid = $USER->id;
 282      }
 283  
 284      // Zero out everything
 285      $ncorrect     = 0;
 286      $nviewed      = 0;
 287      $score        = 0;
 288      $nmanual      = 0;
 289      $manualpoints = 0;
 290      $thegrade     = 0;
 291      $nquestions   = 0;
 292      $total        = 0;
 293      $earned       = 0;
 294  
 295      $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries);
 296      if ($useranswers = $DB->get_records_select("lesson_attempts",  "lessonid = :lessonid AND
 297              userid = :userid AND retry = :retry", $params, "timeseen")) {
 298          // group each try with its page
 299          $attemptset = array();
 300          foreach ($useranswers as $useranswer) {
 301              $attemptset[$useranswer->pageid][] = $useranswer;
 302          }
 303  
 304          if (!empty($lesson->maxattempts)) {
 305              // Drop all attempts that go beyond max attempts for the lesson.
 306              foreach ($attemptset as $key => $set) {
 307                  $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts);
 308              }
 309          }
 310  
 311          // get only the pages and their answers that the user answered
 312          list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset));
 313          array_unshift($parameters, $lesson->id);
 314          $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters);
 315          $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters);
 316  
 317          // Number of pages answered
 318          $nquestions = count($pages);
 319  
 320          foreach ($attemptset as $attempts) {
 321              $page = lesson_page::load($pages[end($attempts)->pageid], $lesson);
 322              if ($lesson->custom) {
 323                  $attempt = end($attempts);
 324                  // If essay question, handle it, otherwise add to score
 325                  if ($page->requires_manual_grading()) {
 326                      $useranswerobj = unserialize_object($attempt->useranswer);
 327                      if (isset($useranswerobj->score)) {
 328                          $earned += $useranswerobj->score;
 329                      }
 330                      $nmanual++;
 331                      $manualpoints += $answers[$attempt->answerid]->score;
 332                  } else if (!empty($attempt->answerid)) {
 333                      $earned += $page->earned_score($answers, $attempt);
 334                  }
 335              } else {
 336                  foreach ($attempts as $attempt) {
 337                      $earned += $attempt->correct;
 338                  }
 339                  $attempt = end($attempts); // doesn't matter which one
 340                  // If essay question, increase numbers
 341                  if ($page->requires_manual_grading()) {
 342                      $nmanual++;
 343                      $manualpoints++;
 344                  }
 345              }
 346              // Number of times answered
 347              $nviewed += count($attempts);
 348          }
 349  
 350          if ($lesson->custom) {
 351              $bestscores = array();
 352              // Find the highest possible score per page to get our total
 353              foreach ($answers as $answer) {
 354                  if(!isset($bestscores[$answer->pageid])) {
 355                      $bestscores[$answer->pageid] = $answer->score;
 356                  } else if ($bestscores[$answer->pageid] < $answer->score) {
 357                      $bestscores[$answer->pageid] = $answer->score;
 358                  }
 359              }
 360              $total = array_sum($bestscores);
 361          } else {
 362              // Check to make sure the student has answered the minimum questions
 363              if ($lesson->minquestions and $nquestions < $lesson->minquestions) {
 364                  // Nope, increase number viewed by the amount of unanswered questions
 365                  $total =  $nviewed + ($lesson->minquestions - $nquestions);
 366              } else {
 367                  $total = $nviewed;
 368              }
 369          }
 370      }
 371  
 372      if ($total) { // not zero
 373          $thegrade = round(100 * $earned / $total, 5);
 374      }
 375  
 376      // Build the grade information object
 377      $gradeinfo               = new stdClass;
 378      $gradeinfo->nquestions   = $nquestions;
 379      $gradeinfo->attempts     = $nviewed;
 380      $gradeinfo->total        = $total;
 381      $gradeinfo->earned       = $earned;
 382      $gradeinfo->grade        = $thegrade;
 383      $gradeinfo->nmanual      = $nmanual;
 384      $gradeinfo->manualpoints = $manualpoints;
 385  
 386      return $gradeinfo;
 387  }
 388  
 389  /**
 390   * Determines if a user can view the left menu.  The determining factor
 391   * is whether a user has a grade greater than or equal to the lesson setting
 392   * of displayleftif
 393   *
 394   * @param object $lesson Lesson object of the current lesson
 395   * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged
 396   **/
 397  function lesson_displayleftif($lesson) {
 398      global $CFG, $USER, $DB;
 399  
 400      if (!empty($lesson->displayleftif)) {
 401          // get the current user's max grade for this lesson
 402          $params = array ("userid" => $USER->id, "lessonid" => $lesson->id);
 403          if ($maxgrade = $DB->get_record_sql('SELECT userid, MAX(grade) AS maxgrade FROM {lesson_grades} WHERE userid = :userid AND lessonid = :lessonid GROUP BY userid', $params)) {
 404              if ($maxgrade->maxgrade < $lesson->displayleftif) {
 405                  return 0;  // turn off the displayleft
 406              }
 407          } else {
 408              return 0; // no grades
 409          }
 410      }
 411  
 412      // if we get to here, keep the original state of displayleft lesson setting
 413      return $lesson->displayleft;
 414  }
 415  
 416  /**
 417   *
 418   * @param $cm
 419   * @param $lesson
 420   * @param $page
 421   * @return unknown_type
 422   */
 423  function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) {
 424      $bc = lesson_menu_block_contents($cm->id, $lesson);
 425      if (!empty($bc)) {
 426          $regions = $page->blocks->get_regions();
 427          $firstregion = reset($regions);
 428          $page->blocks->add_fake_block($bc, $firstregion);
 429      }
 430  
 431      $bc = lesson_mediafile_block_contents($cm->id, $lesson);
 432      if (!empty($bc)) {
 433          $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
 434      }
 435  
 436      if (!empty($timer)) {
 437          $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page);
 438          if (!empty($bc)) {
 439              $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
 440          }
 441      }
 442  }
 443  
 444  /**
 445   * If there is a media file associated with this
 446   * lesson, return a block_contents that displays it.
 447   *
 448   * @param int $cmid Course Module ID for this lesson
 449   * @param object $lesson Full lesson record object
 450   * @return block_contents
 451   **/
 452  function lesson_mediafile_block_contents($cmid, $lesson) {
 453      global $OUTPUT;
 454      if (empty($lesson->mediafile)) {
 455          return null;
 456      }
 457  
 458      $options = array();
 459      $options['menubar'] = 0;
 460      $options['location'] = 0;
 461      $options['left'] = 5;
 462      $options['top'] = 5;
 463      $options['scrollbars'] = 1;
 464      $options['resizable'] = 1;
 465      $options['width'] = $lesson->mediawidth;
 466      $options['height'] = $lesson->mediaheight;
 467  
 468      $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid);
 469      $action = new popup_action('click', $link, 'lessonmediafile', $options);
 470      $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson')));
 471  
 472      $bc = new block_contents();
 473      $bc->title = get_string('linkedmedia', 'lesson');
 474      $bc->attributes['class'] = 'mediafile block';
 475      $bc->content = $content;
 476  
 477      return $bc;
 478  }
 479  
 480  /**
 481   * If a timed lesson and not a teacher, then
 482   * return a block_contents containing the clock.
 483   *
 484   * @param int $cmid Course Module ID for this lesson
 485   * @param object $lesson Full lesson record object
 486   * @param object $timer Full timer record object
 487   * @return block_contents
 488   **/
 489  function lesson_clock_block_contents($cmid, $lesson, $timer, $page) {
 490      // Display for timed lessons and for students only
 491      $context = context_module::instance($cmid);
 492      if ($lesson->timelimit == 0 || has_capability('mod/lesson:manage', $context)) {
 493          return null;
 494      }
 495  
 496      $content = '<div id="lesson-timer">';
 497      $content .=  $lesson->time_remaining($timer->starttime);
 498      $content .= '</div>';
 499  
 500      $clocksettings = array('starttime' => $timer->starttime, 'servertime' => time(), 'testlength' => $lesson->timelimit);
 501      $page->requires->data_for_js('clocksettings', $clocksettings, true);
 502      $page->requires->strings_for_js(array('timeisup'), 'lesson');
 503      $page->requires->js('/mod/lesson/timer.js');
 504      $page->requires->js_init_call('show_clock');
 505  
 506      $bc = new block_contents();
 507      $bc->title = get_string('timeremaining', 'lesson');
 508      $bc->attributes['class'] = 'clock block';
 509      $bc->content = $content;
 510  
 511      return $bc;
 512  }
 513  
 514  /**
 515   * If left menu is turned on, then this will
 516   * print the menu in a block
 517   *
 518   * @param int $cmid Course Module ID for this lesson
 519   * @param lesson $lesson Full lesson record object
 520   * @return void
 521   **/
 522  function lesson_menu_block_contents($cmid, $lesson) {
 523      global $CFG, $DB;
 524  
 525      if (!$lesson->displayleft) {
 526          return null;
 527      }
 528  
 529      $pages = $lesson->load_all_pages();
 530      foreach ($pages as $page) {
 531          if ((int)$page->prevpageid === 0) {
 532              $pageid = $page->id;
 533              break;
 534          }
 535      }
 536      $currentpageid = optional_param('pageid', $pageid, PARAM_INT);
 537  
 538      if (!$pageid || !$pages) {
 539          return null;
 540      }
 541  
 542      $content = '<a href="#maincontent" class="accesshide">' .
 543          get_string('skip', 'lesson') .
 544          "</a>\n<div class=\"menuwrapper\">\n<ul>\n";
 545  
 546      while ($pageid != 0) {
 547          $page = $pages[$pageid];
 548  
 549          // Only process branch tables with display turned on
 550          if ($page->displayinmenublock && $page->display) {
 551              if ($page->id == $currentpageid) {
 552                  $content .= '<li class="selected">'.format_string($page->title,true)."</li>\n";
 553              } else {
 554                  $content .= "<li class=\"notselected\"><a href=\"$CFG->wwwroot/mod/lesson/view.php?id=$cmid&amp;pageid=$page->id\">".format_string($page->title,true)."</a></li>\n";
 555              }
 556  
 557          }
 558          $pageid = $page->nextpageid;
 559      }
 560      $content .= "</ul>\n</div>\n";
 561  
 562      $bc = new block_contents();
 563      $bc->title = get_string('lessonmenu', 'lesson');
 564      $bc->attributes['class'] = 'menu block';
 565      $bc->content = $content;
 566  
 567      return $bc;
 568  }
 569  
 570  /**
 571   * Adds header buttons to the page for the lesson
 572   *
 573   * @param object $cm
 574   * @param object $context
 575   * @param bool $extraeditbuttons
 576   * @param int $lessonpageid
 577   */
 578  function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $lessonpageid=null) {
 579      global $CFG, $PAGE, $OUTPUT;
 580      if (has_capability('mod/lesson:edit', $context) && $extraeditbuttons) {
 581          if ($lessonpageid === null) {
 582              print_error('invalidpageid', 'lesson');
 583          }
 584          if (!empty($lessonpageid) && $lessonpageid != LESSON_EOL) {
 585              $url = new moodle_url('/mod/lesson/editpage.php', array(
 586                  'id'       => $cm->id,
 587                  'pageid'   => $lessonpageid,
 588                  'edit'     => 1,
 589                  'returnto' => $PAGE->url->out_as_local_url(false)
 590              ));
 591              $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
 592          }
 593      }
 594  }
 595  
 596  /**
 597   * This is a function used to detect media types and generate html code.
 598   *
 599   * @global object $CFG
 600   * @global object $PAGE
 601   * @param object $lesson
 602   * @param object $context
 603   * @return string $code the html code of media
 604   */
 605  function lesson_get_media_html($lesson, $context) {
 606      global $CFG, $PAGE, $OUTPUT;
 607      require_once("$CFG->libdir/resourcelib.php");
 608  
 609      // get the media file link
 610      if (strpos($lesson->mediafile, '://') !== false) {
 611          $url = new moodle_url($lesson->mediafile);
 612      } else {
 613          // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder
 614          $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/'));
 615      }
 616      $title = $lesson->mediafile;
 617  
 618      $clicktoopen = html_writer::link($url, get_string('download'));
 619  
 620      $mimetype = resourcelib_guess_url_mimetype($url);
 621  
 622      $extension = resourcelib_get_extension($url->out(false));
 623  
 624      $mediamanager = core_media_manager::instance($PAGE);
 625      $embedoptions = array(
 626          core_media_manager::OPTION_TRUSTED => true,
 627          core_media_manager::OPTION_BLOCK => true
 628      );
 629  
 630      // find the correct type and print it out
 631      if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) {  // It's an image
 632          $code = resourcelib_embed_image($url, $title);
 633  
 634      } else if ($mediamanager->can_embed_url($url, $embedoptions)) {
 635          // Media (audio/video) file.
 636          $code = $mediamanager->embed_url($url, $title, 0, 0, $embedoptions);
 637  
 638      } else {
 639          // anything else - just try object tag enlarged as much as possible
 640          $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype);
 641      }
 642  
 643      return $code;
 644  }
 645  
 646  /**
 647   * Logic to happen when a/some group(s) has/have been deleted in a course.
 648   *
 649   * @param int $courseid The course ID.
 650   * @param int $groupid The group id if it is known
 651   * @return void
 652   */
 653  function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
 654      global $DB;
 655  
 656      $params = array('courseid' => $courseid);
 657      if ($groupid) {
 658          $params['groupid'] = $groupid;
 659          // We just update the group that was deleted.
 660          $sql = "SELECT o.id, o.lessonid, o.groupid
 661                    FROM {lesson_overrides} o
 662                    JOIN {lesson} lesson ON lesson.id = o.lessonid
 663                   WHERE lesson.course = :courseid
 664                     AND o.groupid = :groupid";
 665      } else {
 666          // No groupid, we update all orphaned group overrides for all lessons in course.
 667          $sql = "SELECT o.id, o.lessonid, o.groupid
 668                    FROM {lesson_overrides} o
 669                    JOIN {lesson} lesson ON lesson.id = o.lessonid
 670               LEFT JOIN {groups} grp ON grp.id = o.groupid
 671                   WHERE lesson.course = :courseid
 672                     AND o.groupid IS NOT NULL
 673                     AND grp.id IS NULL";
 674      }
 675      $records = $DB->get_records_sql($sql, $params);
 676      if (!$records) {
 677          return; // Nothing to do.
 678      }
 679      $DB->delete_records_list('lesson_overrides', 'id', array_keys($records));
 680      $cache = cache::make('mod_lesson', 'overrides');
 681      foreach ($records as $record) {
 682          $cache->delete("{$record->lessonid}_g_{$record->groupid}");
 683      }
 684  }
 685  
 686  /**
 687   * Return the overview report table and data.
 688   *
 689   * @param  lesson $lesson       lesson instance
 690   * @param  mixed $currentgroup  false if not group used, 0 for all groups, group id (int) to filter by that groups
 691   * @return mixed false if there is no information otherwise html_table and stdClass with the table and data
 692   * @since  Moodle 3.3
 693   */
 694  function lesson_get_overview_report_table_and_data(lesson $lesson, $currentgroup) {
 695      global $DB, $CFG, $OUTPUT;
 696      require_once($CFG->dirroot . '/mod/lesson/pagetypes/branchtable.php');
 697  
 698      $context = $lesson->context;
 699      $cm = $lesson->cm;
 700      // Count the number of branch and question pages in this lesson.
 701      $branchcount = $DB->count_records('lesson_pages', array('lessonid' => $lesson->id, 'qtype' => LESSON_PAGE_BRANCHTABLE));
 702      $questioncount = ($DB->count_records('lesson_pages', array('lessonid' => $lesson->id)) - $branchcount);
 703  
 704      // Only load students if there attempts for this lesson.
 705      $attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id));
 706      $branches = $DB->record_exists('lesson_branch', array('lessonid' => $lesson->id));
 707      $timer = $DB->record_exists('lesson_timer', array('lessonid' => $lesson->id));
 708      if ($attempts or $branches or $timer) {
 709          list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
 710          list($sort, $sortparams) = users_order_by_sql('u');
 711  
 712          // TODO Does not support custom user profile fields (MDL-70456).
 713          $userfieldsapi = \core_user\fields::for_identity($context, false)->with_userpic();
 714          $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
 715          $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
 716  
 717          $params['a1lessonid'] = $lesson->id;
 718          $params['b1lessonid'] = $lesson->id;
 719          $params['c1lessonid'] = $lesson->id;
 720          $sql = "SELECT DISTINCT $ufields
 721                  FROM {user} u
 722                  JOIN (
 723                      SELECT userid, lessonid FROM {lesson_attempts} a1
 724                      WHERE a1.lessonid = :a1lessonid
 725                          UNION
 726                      SELECT userid, lessonid FROM {lesson_branch} b1
 727                      WHERE b1.lessonid = :b1lessonid
 728                          UNION
 729                      SELECT userid, lessonid FROM {lesson_timer} c1
 730                      WHERE c1.lessonid = :c1lessonid
 731                      ) a ON u.id = a.userid
 732                  JOIN ($esql) ue ON ue.id = a.userid
 733                  ORDER BY $sort";
 734  
 735          $students = $DB->get_recordset_sql($sql, $params);
 736          if (!$students->valid()) {
 737              $students->close();
 738              return array(false, false);
 739          }
 740      } else {
 741          return array(false, false);
 742      }
 743  
 744      if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
 745          $grades = array();
 746      }
 747  
 748      if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
 749          $times = array();
 750      }
 751  
 752      // Build an array for output.
 753      $studentdata = array();
 754  
 755      $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
 756      foreach ($attempts as $attempt) {
 757          // if the user is not in the array or if the retry number is not in the sub array, add the data for that try.
 758          if (empty($studentdata[$attempt->userid]) || empty($studentdata[$attempt->userid][$attempt->retry])) {
 759              // restore/setup defaults
 760              $n = 0;
 761              $timestart = 0;
 762              $timeend = 0;
 763              $usergrade = null;
 764              $eol = 0;
 765  
 766              // search for the grade record for this try. if not there, the nulls defined above will be used.
 767              foreach($grades as $grade) {
 768                  // check to see if the grade matches the correct user
 769                  if ($grade->userid == $attempt->userid) {
 770                      // see if n is = to the retry
 771                      if ($n == $attempt->retry) {
 772                          // get grade info
 773                          $usergrade = round($grade->grade, 2); // round it here so we only have to do it once
 774                          break;
 775                      }
 776                      $n++; // if not equal, then increment n
 777                  }
 778              }
 779              $n = 0;
 780              // search for the time record for this try. if not there, the nulls defined above will be used.
 781              foreach($times as $time) {
 782                  // check to see if the grade matches the correct user
 783                  if ($time->userid == $attempt->userid) {
 784                      // see if n is = to the retry
 785                      if ($n == $attempt->retry) {
 786                          // get grade info
 787                          $timeend = $time->lessontime;
 788                          $timestart = $time->starttime;
 789                          $eol = $time->completed;
 790                          break;
 791                      }
 792                      $n++; // if not equal, then increment n
 793                  }
 794              }
 795  
 796              // build up the array.
 797              // this array represents each student and all of their tries at the lesson
 798              $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart,
 799                                                                      "timeend" => $timeend,
 800                                                                      "grade" => $usergrade,
 801                                                                      "end" => $eol,
 802                                                                      "try" => $attempt->retry,
 803                                                                      "userid" => $attempt->userid);
 804          }
 805      }
 806      $attempts->close();
 807  
 808      $branches = $DB->get_recordset('lesson_branch', array('lessonid' => $lesson->id), 'timeseen');
 809      foreach ($branches as $branch) {
 810          // If the user is not in the array or if the retry number is not in the sub array, add the data for that try.
 811          if (empty($studentdata[$branch->userid]) || empty($studentdata[$branch->userid][$branch->retry])) {
 812              // Restore/setup defaults.
 813              $n = 0;
 814              $timestart = 0;
 815              $timeend = 0;
 816              $usergrade = null;
 817              $eol = 0;
 818              // Search for the time record for this try. if not there, the nulls defined above will be used.
 819              foreach ($times as $time) {
 820                  // Check to see if the grade matches the correct user.
 821                  if ($time->userid == $branch->userid) {
 822                      // See if n is = to the retry.
 823                      if ($n == $branch->retry) {
 824                          // Get grade info.
 825                          $timeend = $time->lessontime;
 826                          $timestart = $time->starttime;
 827                          $eol = $time->completed;
 828                          break;
 829                      }
 830                      $n++; // If not equal, then increment n.
 831                  }
 832              }
 833  
 834              // Build up the array.
 835              // This array represents each student and all of their tries at the lesson.
 836              $studentdata[$branch->userid][$branch->retry] = array( "timestart" => $timestart,
 837                                                                      "timeend" => $timeend,
 838                                                                      "grade" => $usergrade,
 839                                                                      "end" => $eol,
 840                                                                      "try" => $branch->retry,
 841                                                                      "userid" => $branch->userid);
 842          }
 843      }
 844      $branches->close();
 845  
 846      // Need the same thing for timed entries that were not completed.
 847      foreach ($times as $time) {
 848          $endoflesson = $time->completed;
 849          // If the time start is the same with another record then we shouldn't be adding another item to this array.
 850          if (isset($studentdata[$time->userid])) {
 851              $foundmatch = false;
 852              $n = 0;
 853              foreach ($studentdata[$time->userid] as $key => $value) {
 854                  if ($value['timestart'] == $time->starttime) {
 855                      // Don't add this to the array.
 856                      $foundmatch = true;
 857                      break;
 858                  }
 859              }
 860              $n = count($studentdata[$time->userid]) + 1;
 861              if (!$foundmatch) {
 862                  // Add a record.
 863                  $studentdata[$time->userid][] = array(
 864                                  "timestart" => $time->starttime,
 865                                  "timeend" => $time->lessontime,
 866                                  "grade" => null,
 867                                  "end" => $endoflesson,
 868                                  "try" => $n,
 869                                  "userid" => $time->userid
 870                              );
 871              }
 872          } else {
 873              $studentdata[$time->userid][] = array(
 874                                  "timestart" => $time->starttime,
 875                                  "timeend" => $time->lessontime,
 876                                  "grade" => null,
 877                                  "end" => $endoflesson,
 878                                  "try" => 0,
 879                                  "userid" => $time->userid
 880                              );
 881          }
 882      }
 883  
 884      // To store all the data to be returned by the function.
 885      $data = new stdClass();
 886  
 887      // Determine if lesson should have a score.
 888      if ($branchcount > 0 AND $questioncount == 0) {
 889          // This lesson only contains content pages and is not graded.
 890          $data->lessonscored = false;
 891      } else {
 892          // This lesson is graded.
 893          $data->lessonscored = true;
 894      }
 895      // set all the stats variables
 896      $data->numofattempts = 0;
 897      $data->avescore      = 0;
 898      $data->avetime       = 0;
 899      $data->highscore     = null;
 900      $data->lowscore      = null;
 901      $data->hightime      = null;
 902      $data->lowtime       = null;
 903      $data->students      = array();
 904  
 905      $table = new html_table();
 906  
 907      $headers = [get_string('name')];
 908  
 909      foreach ($extrafields as $field) {
 910          $headers[] = \core_user\fields::get_display_name($field);
 911      }
 912  
 913      $caneditlesson = has_capability('mod/lesson:edit', $context);
 914      $attemptsheader = get_string('attempts', 'lesson');
 915      if ($caneditlesson) {
 916          $selectall = get_string('selectallattempts', 'lesson');
 917          $deselectall = get_string('deselectallattempts', 'lesson');
 918          // Build the select/deselect all control.
 919          $selectallid = 'selectall-attempts';
 920          $mastercheckbox = new \core\output\checkbox_toggleall('lesson-attempts', true, [
 921              'id' => $selectallid,
 922              'name' => $selectallid,
 923              'value' => 1,
 924              'label' => $selectall,
 925              'selectall' => $selectall,
 926              'deselectall' => $deselectall,
 927              'labelclasses' => 'form-check-label'
 928          ]);
 929          $attemptsheader = $OUTPUT->render($mastercheckbox);
 930      }
 931      $headers [] = $attemptsheader;
 932  
 933      // Set up the table object.
 934      if ($data->lessonscored) {
 935          $headers [] = get_string('highscore', 'lesson');
 936      }
 937  
 938      $colcount = count($headers);
 939  
 940      $table->head = $headers;
 941  
 942      $table->align = [];
 943      $table->align = array_pad($table->align, $colcount, 'center');
 944      $table->align[$colcount - 1] = 'left';
 945  
 946      if ($data->lessonscored) {
 947          $table->align[$colcount - 2] = 'left';
 948      }
 949  
 950      $table->attributes['class'] = 'table table-striped';
 951  
 952      // print out the $studentdata array
 953      // going through each student that has attempted the lesson, so, each student should have something to be displayed
 954      foreach ($students as $student) {
 955          // check to see if the student has attempts to print out
 956          if (array_key_exists($student->id, $studentdata)) {
 957              // set/reset some variables
 958              $attempts = array();
 959              $dataforstudent = new stdClass;
 960              $dataforstudent->attempts = array();
 961              // gather the data for each user attempt
 962              $bestgrade = 0;
 963  
 964              // $tries holds all the tries/retries a student has done
 965              $tries = $studentdata[$student->id];
 966              $studentname = fullname($student, true);
 967  
 968              foreach ($tries as $try) {
 969                  $dataforstudent->attempts[] = $try;
 970  
 971                  // Start to build up the checkbox and link.
 972                  $attempturlparams = [
 973                      'id' => $cm->id,
 974                      'action' => 'reportdetail',
 975                      'userid' => $try['userid'],
 976                      'try' => $try['try'],
 977                  ];
 978  
 979                  if ($try["grade"] !== null) { // if null then not done yet
 980                      // this is what the link does when the user has completed the try
 981                      $timetotake = $try["timeend"] - $try["timestart"];
 982  
 983                      if ($try["grade"] > $bestgrade) {
 984                          $bestgrade = $try["grade"];
 985                      }
 986  
 987                      $attemptdata = (object)[
 988                          'grade' => $try["grade"],
 989                          'timestart' => userdate($try["timestart"]),
 990                          'duration' => format_time($timetotake),
 991                      ];
 992                      $attemptlinkcontents = get_string('attemptinfowithgrade', 'lesson', $attemptdata);
 993  
 994                  } else {
 995                      if ($try["end"]) {
 996                          // User finished the lesson but has no grade. (Happens when there are only content pages).
 997                          $timetotake = $try["timeend"] - $try["timestart"];
 998                          $attemptdata = (object)[
 999                              'timestart' => userdate($try["timestart"]),
1000                              'duration' => format_time($timetotake),
1001                          ];
1002                          $attemptlinkcontents = get_string('attemptinfonograde', 'lesson', $attemptdata);
1003                      } else {
1004                          // This is what the link does/looks like when the user has not completed the attempt.
1005                          if ($try['timestart'] !== 0) {
1006                              // Teacher previews do not track time spent.
1007                              $attemptlinkcontents = get_string("notcompletedwithdate", "lesson", userdate($try["timestart"]));
1008                          } else {
1009                              $attemptlinkcontents = get_string("notcompleted", "lesson");
1010                          }
1011                          $timetotake = null;
1012                      }
1013                  }
1014                  $attempturl = new moodle_url('/mod/lesson/report.php', $attempturlparams);
1015                  $attemptlink = html_writer::link($attempturl, $attemptlinkcontents, ['class' => 'lesson-attempt-link']);
1016  
1017                  if ($caneditlesson) {
1018                      $attemptid = 'attempt-' . $try['userid'] . '-' . $try['try'];
1019                      $attemptname = 'attempts[' . $try['userid'] . '][' . $try['try'] . ']';
1020  
1021                      $checkbox = new \core\output\checkbox_toggleall('lesson-attempts', false, [
1022                          'id' => $attemptid,
1023                          'name' => $attemptname,
1024                          'label' => $attemptlink
1025                      ]);
1026                      $attemptlink = $OUTPUT->render($checkbox);
1027                  }
1028  
1029                  // build up the attempts array
1030                  $attempts[] = $attemptlink;
1031  
1032                  // Run these lines for the stats only if the user finnished the lesson.
1033                  if ($try["end"]) {
1034                      // User has completed the lesson.
1035                      $data->numofattempts++;
1036                      $data->avetime += $timetotake;
1037                      if ($timetotake > $data->hightime || $data->hightime == null) {
1038                          $data->hightime = $timetotake;
1039                      }
1040                      if ($timetotake < $data->lowtime || $data->lowtime == null) {
1041                          $data->lowtime = $timetotake;
1042                      }
1043                      if ($try["grade"] !== null) {
1044                          // The lesson was scored.
1045                          $data->avescore += $try["grade"];
1046                          if ($try["grade"] > $data->highscore || $data->highscore === null) {
1047                              $data->highscore = $try["grade"];
1048                          }
1049                          if ($try["grade"] < $data->lowscore || $data->lowscore === null) {
1050                              $data->lowscore = $try["grade"];
1051                          }
1052  
1053                      }
1054                  }
1055              }
1056              // get line breaks in after each attempt
1057              $attempts = implode("<br />\n", $attempts);
1058              $row = [$studentname];
1059  
1060              foreach ($extrafields as $field) {
1061                  $row[] = $student->$field;
1062              }
1063  
1064              $row[] = $attempts;
1065  
1066              if ($data->lessonscored) {
1067                  // Add the grade if the lesson is graded.
1068                  $row[] = $bestgrade."%";
1069              }
1070  
1071              $table->data[] = $row;
1072  
1073              // Add the student data.
1074              $dataforstudent->id = $student->id;
1075              $dataforstudent->fullname = $studentname;
1076              $dataforstudent->bestgrade = $bestgrade;
1077              $data->students[] = $dataforstudent;
1078          }
1079      }
1080      $students->close();
1081      if ($data->numofattempts > 0) {
1082          $data->avescore = $data->avescore / $data->numofattempts;
1083      }
1084  
1085      return array($table, $data);
1086  }
1087  
1088  /**
1089   * Return information about one user attempt (including answers)
1090   * @param  lesson $lesson  lesson instance
1091   * @param  int $userid     the user id
1092   * @param  int $attempt    the attempt number
1093   * @return array the user answers (array) and user data stats (object)
1094   * @since  Moodle 3.3
1095   */
1096  function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt) {
1097      global $DB;
1098  
1099      $context = $lesson->context;
1100      if (!empty($userid)) {
1101          // Apply overrides.
1102          $lesson->update_effective_access($userid);
1103      }
1104  
1105      $pageid = 0;
1106      $lessonpages = $lesson->load_all_pages();
1107      foreach ($lessonpages as $lessonpage) {
1108          if ($lessonpage->prevpageid == 0) {
1109              $pageid = $lessonpage->id;
1110          }
1111      }
1112  
1113      // now gather the stats into an object
1114      $firstpageid = $pageid;
1115      $pagestats = array();
1116      while ($pageid != 0) { // EOL
1117          $page = $lessonpages[$pageid];
1118          $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
1119          if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
1120              // get them ready for processing
1121              $orderedanswers = array();
1122              foreach ($allanswers as $singleanswer) {
1123                  // ordering them like this, will help to find the single attempt record that we want to keep.
1124                  $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
1125              }
1126              // this is foreach user and for each try for that user, keep one attempt record
1127              foreach ($orderedanswers as $orderedanswer) {
1128                  foreach($orderedanswer as $tries) {
1129                      $page->stats($pagestats, $tries);
1130                  }
1131              }
1132          } else {
1133              // no one answered yet...
1134          }
1135          //unset($orderedanswers);  initialized above now
1136          $pageid = $page->nextpageid;
1137      }
1138  
1139      $manager = lesson_page_type_manager::get($lesson);
1140      $qtypes = $manager->get_page_type_strings();
1141  
1142      $answerpages = array();
1143      $answerpage = "";
1144      $pageid = $firstpageid;
1145      // cycle through all the pages
1146      //  foreach page, add to the $answerpages[] array all the data that is needed
1147      //  from the question, the users attempt, and the statistics
1148      // grayout pages that the user did not answer and Branch, end of branch, cluster
1149      // and end of cluster pages
1150      while ($pageid != 0) { // EOL
1151          $page = $lessonpages[$pageid];
1152          $answerpage = new stdClass;
1153          // Keep the original page object.
1154          $answerpage->page = $page;
1155          $data ='';
1156  
1157          $answerdata = new stdClass;
1158          // Set some defaults for the answer data.
1159          $answerdata->score = null;
1160          $answerdata->response = null;
1161          $answerdata->responseformat = FORMAT_PLAIN;
1162  
1163          $answerpage->title = format_string($page->title);
1164  
1165          $options = new stdClass;
1166          $options->noclean = true;
1167          $options->overflowdiv = true;
1168          $options->context = $context;
1169          $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
1170  
1171          $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
1172          $answerpage->grayout = $page->grayout;
1173          $answerpage->context = $context;
1174  
1175          if (empty($userid)) {
1176              // there is no userid, so set these vars and display stats.
1177              $answerpage->grayout = 0;
1178              $useranswer = null;
1179          } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$attempt,"pageid"=>$page->id), "timeseen")) {
1180              // get the user's answer for this page
1181              // need to find the right one
1182              $i = 0;
1183              foreach ($useranswers as $userattempt) {
1184                  $useranswer = $userattempt;
1185                  $i++;
1186                  if ($lesson->maxattempts == $i) {
1187                      break; // reached maxattempts, break out
1188                  }
1189              }
1190          } else {
1191              // user did not answer this page, gray it out and set some nulls
1192              $answerpage->grayout = 1;
1193              $useranswer = null;
1194          }
1195          $i = 0;
1196          $n = 0;
1197          $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
1198          $pageid = $page->nextpageid;
1199      }
1200  
1201      $userstats = new stdClass;
1202      if (!empty($userid)) {
1203          $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
1204  
1205          $alreadycompleted = true;
1206  
1207          if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $attempt, 1)) {
1208              $userstats->grade = -1;
1209              $userstats->completed = -1;
1210              $alreadycompleted = false;
1211          } else {
1212              $userstats->grade = current($grades);
1213              $userstats->completed = $userstats->grade->completed;
1214              $userstats->grade = round($userstats->grade->grade, 2);
1215          }
1216  
1217          if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $attempt, 1)) {
1218              $userstats->timetotake = -1;
1219              $alreadycompleted = false;
1220          } else {
1221              $userstats->timetotake = current($times);
1222              $userstats->timetotake = $userstats->timetotake->lessontime - $userstats->timetotake->starttime;
1223          }
1224  
1225          if ($alreadycompleted) {
1226              $userstats->gradeinfo = lesson_grade($lesson, $attempt, $userid);
1227          }
1228      }
1229  
1230      return array($answerpages, $userstats);
1231  }
1232  
1233  /**
1234   * Return user's deadline for all lessons in a course, hereby taking into account group and user overrides.
1235   *
1236   * @param int $courseid the course id.
1237   * @return object An object with of all lessonsids and close unixdates in this course,
1238   * taking into account the most lenient overrides, if existing and 0 if no close date is set.
1239   */
1240  function lesson_get_user_deadline($courseid) {
1241      global $DB, $USER;
1242  
1243      // For teacher and manager/admins return lesson's deadline.
1244      if (has_capability('moodle/course:update', context_course::instance($courseid))) {
1245          $sql = "SELECT lesson.id, lesson.deadline AS userdeadline
1246                    FROM {lesson} lesson
1247                   WHERE lesson.course = :courseid";
1248  
1249          $results = $DB->get_records_sql($sql, array('courseid' => $courseid));
1250          return $results;
1251      }
1252  
1253      $sql = "SELECT a.id,
1254                     COALESCE(v.userclose, v.groupclose, a.deadline, 0) AS userdeadline
1255                FROM (
1256                        SELECT lesson.id as lessonid,
1257                               MAX(leo.deadline) AS userclose, MAX(qgo.deadline) AS groupclose
1258                          FROM {lesson} lesson
1259                     LEFT JOIN {lesson_overrides} leo on lesson.id = leo.lessonid AND leo.userid = :userid
1260                     LEFT JOIN {groups_members} gm ON gm.userid = :useringroupid
1261                     LEFT JOIN {lesson_overrides} qgo on lesson.id = qgo.lessonid AND qgo.groupid = gm.groupid
1262                         WHERE lesson.course = :courseid
1263                      GROUP BY lesson.id
1264                     ) v
1265                JOIN {lesson} a ON a.id = v.lessonid";
1266  
1267      $results = $DB->get_records_sql($sql, array('userid' => $USER->id, 'useringroupid' => $USER->id, 'courseid' => $courseid));
1268      return $results;
1269  
1270  }
1271  
1272  /**
1273   * Abstract class that page type's MUST inherit from.
1274   *
1275   * This is the abstract class that ALL add page type forms must extend.
1276   * You will notice that all but two of the methods this class contains are final.
1277   * Essentially the only thing that extending classes can do is extend custom_definition.
1278   * OR if it has a special requirement on creation it can extend construction_override
1279   *
1280   * @abstract
1281   * @copyright  2009 Sam Hemelryk
1282   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1283   */
1284  abstract class lesson_add_page_form_base extends moodleform {
1285  
1286      /**
1287       * This is the classic define that is used to identify this pagetype.
1288       * Will be one of LESSON_*
1289       * @var int
1290       */
1291      public $qtype;
1292  
1293      /**
1294       * The simple string that describes the page type e.g. truefalse, multichoice
1295       * @var string
1296       */
1297      public $qtypestring;
1298  
1299      /**
1300       * An array of options used in the htmleditor
1301       * @var array
1302       */
1303      protected $editoroptions = array();
1304  
1305      /**
1306       * True if this is a standard page of false if it does something special.
1307       * Questions are standard pages, branch tables are not
1308       * @var bool
1309       */
1310      protected $standard = true;
1311  
1312      /**
1313       * Answer format supported by question type.
1314       */
1315      protected $answerformat = '';
1316  
1317      /**
1318       * Response format supported by question type.
1319       */
1320      protected $responseformat = '';
1321  
1322      /**
1323       * Each page type can and should override this to add any custom elements to
1324       * the basic form that they want
1325       */
1326      public function custom_definition() {}
1327  
1328      /**
1329       * Returns answer format used by question type.
1330       */
1331      public function get_answer_format() {
1332          return $this->answerformat;
1333      }
1334  
1335      /**
1336       * Returns response format used by question type.
1337       */
1338      public function get_response_format() {
1339          return $this->responseformat;
1340      }
1341  
1342      /**
1343       * Used to determine if this is a standard page or a special page
1344       * @return bool
1345       */
1346      public final function is_standard() {
1347          return (bool)$this->standard;
1348      }
1349  
1350      /**
1351       * Add the required basic elements to the form.
1352       *
1353       * This method adds the basic elements to the form including title and contents
1354       * and then calls custom_definition();
1355       */
1356      public final function definition() {
1357          global $CFG;
1358          $mform = $this->_form;
1359          $editoroptions = $this->_customdata['editoroptions'];
1360  
1361          if ($this->qtypestring != 'selectaqtype') {
1362              if ($this->_customdata['edit']) {
1363                  $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson'));
1364              } else {
1365                  $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson'));
1366              }
1367          }
1368  
1369          if (!empty($this->_customdata['returnto'])) {
1370              $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
1371              $mform->setType('returnto', PARAM_LOCALURL);
1372          }
1373  
1374          $mform->addElement('hidden', 'id');
1375          $mform->setType('id', PARAM_INT);
1376  
1377          $mform->addElement('hidden', 'pageid');
1378          $mform->setType('pageid', PARAM_INT);
1379  
1380          if ($this->standard === true) {
1381              $mform->addElement('hidden', 'qtype');
1382              $mform->setType('qtype', PARAM_INT);
1383  
1384              $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), array('size'=>70));
1385              $mform->addRule('title', get_string('required'), 'required', null, 'client');
1386              if (!empty($CFG->formatstringstriptags)) {
1387                  $mform->setType('title', PARAM_TEXT);
1388              } else {
1389                  $mform->setType('title', PARAM_CLEANHTML);
1390              }
1391  
1392              $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
1393              $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions);
1394              $mform->setType('contents_editor', PARAM_RAW);
1395              $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client');
1396          }
1397  
1398          $this->custom_definition();
1399  
1400          if ($this->_customdata['edit'] === true) {
1401              $mform->addElement('hidden', 'edit', 1);
1402              $mform->setType('edit', PARAM_BOOL);
1403              $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
1404          } else if ($this->qtype === 'questiontype') {
1405              $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson'));
1406          } else {
1407              $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
1408          }
1409      }
1410  
1411      /**
1412       * Convenience function: Adds a jumpto select element
1413       *
1414       * @param string $name
1415       * @param string|null $label
1416       * @param int $selected The page to select by default
1417       */
1418      protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
1419          $title = get_string("jump", "lesson");
1420          if ($label === null) {
1421              $label = $title;
1422          }
1423          if (is_int($name)) {
1424              $name = "jumpto[$name]";
1425          }
1426          $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
1427          $this->_form->setDefault($name, $selected);
1428          $this->_form->addHelpButton($name, 'jumps', 'lesson');
1429      }
1430  
1431      /**
1432       * Convenience function: Adds a score input element
1433       *
1434       * @param string $name
1435       * @param string|null $label
1436       * @param mixed $value The default value
1437       */
1438      protected final function add_score($name, $label=null, $value=null) {
1439          if ($label === null) {
1440              $label = get_string("score", "lesson");
1441          }
1442  
1443          if (is_int($name)) {
1444              $name = "score[$name]";
1445          }
1446          $this->_form->addElement('text', $name, $label, array('size'=>5));
1447          $this->_form->setType($name, PARAM_INT);
1448          if ($value !== null) {
1449              $this->_form->setDefault($name, $value);
1450          }
1451          $this->_form->addHelpButton($name, 'score', 'lesson');
1452  
1453          // Score is only used for custom scoring. Disable the element when not in use to stop some confusion.
1454          if (!$this->_customdata['lesson']->custom) {
1455              $this->_form->freeze($name);
1456          }
1457      }
1458  
1459      /**
1460       * Convenience function: Adds an answer editor
1461       *
1462       * @param int $count The count of the element to add
1463       * @param string $label, null means default
1464       * @param bool $required
1465       * @param string $format
1466       * @param array $help Add help text via the addHelpButton. Must be an array which contains the string identifier and
1467       *                      component as it's elements
1468       * @return void
1469       */
1470      protected final function add_answer($count, $label = null, $required = false, $format= '', array $help = []) {
1471          if ($label === null) {
1472              $label = get_string('answer', 'lesson');
1473          }
1474  
1475          if ($format == LESSON_ANSWER_HTML) {
1476              $this->_form->addElement('editor', 'answer_editor['.$count.']', $label,
1477                      array('rows' => '4', 'columns' => '80'),
1478                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
1479              $this->_form->setType('answer_editor['.$count.']', PARAM_RAW);
1480              $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
1481          } else {
1482              $this->_form->addElement('text', 'answer_editor['.$count.']', $label,
1483                  array('size' => '50', 'maxlength' => '200'));
1484              $this->_form->setType('answer_editor['.$count.']', PARAM_TEXT);
1485          }
1486  
1487          if ($required) {
1488              $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client');
1489          }
1490  
1491          if ($help) {
1492              $this->_form->addHelpButton("answer_editor[$count]", $help['identifier'], $help['component']);
1493          }
1494      }
1495      /**
1496       * Convenience function: Adds an response editor
1497       *
1498       * @param int $count The count of the element to add
1499       * @param string $label, null means default
1500       * @param bool $required
1501       * @return void
1502       */
1503      protected final function add_response($count, $label = null, $required = false) {
1504          if ($label === null) {
1505              $label = get_string('response', 'lesson');
1506          }
1507          $this->_form->addElement('editor', 'response_editor['.$count.']', $label,
1508                   array('rows' => '4', 'columns' => '80'),
1509                   array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
1510          $this->_form->setType('response_editor['.$count.']', PARAM_RAW);
1511          $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
1512  
1513          if ($required) {
1514              $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
1515          }
1516      }
1517  
1518      /**
1519       * A function that gets called upon init of this object by the calling script.
1520       *
1521       * This can be used to process an immediate action if required. Currently it
1522       * is only used in special cases by non-standard page types.
1523       *
1524       * @return bool
1525       */
1526      public function construction_override($pageid, lesson $lesson) {
1527          return true;
1528      }
1529  }
1530  
1531  
1532  
1533  /**
1534   * Class representation of a lesson
1535   *
1536   * This class is used the interact with, and manage a lesson once instantiated.
1537   * If you need to fetch a lesson object you can do so by calling
1538   *
1539   * <code>
1540   * lesson::load($lessonid);
1541   * // or
1542   * $lessonrecord = $DB->get_record('lesson', $lessonid);
1543   * $lesson = new lesson($lessonrecord);
1544   * </code>
1545   *
1546   * The class itself extends lesson_base as all classes within the lesson module should
1547   *
1548   * These properties are from the database
1549   * @property int $id The id of this lesson
1550   * @property int $course The ID of the course this lesson belongs to
1551   * @property string $name The name of this lesson
1552   * @property int $practice Flag to toggle this as a practice lesson
1553   * @property int $modattempts Toggle to allow the user to go back and review answers
1554   * @property int $usepassword Toggle the use of a password for entry
1555   * @property string $password The password to require users to enter
1556   * @property int $dependency ID of another lesson this lesson is dependent on
1557   * @property string $conditions Conditions of the lesson dependency
1558   * @property int $grade The maximum grade a user can achieve (%)
1559   * @property int $custom Toggle custom scoring on or off
1560   * @property int $ongoing Toggle display of an ongoing score
1561   * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
1562   * @property int $maxanswers The max number of answers or branches
1563   * @property int $maxattempts The maximum number of attempts a user can record
1564   * @property int $review Toggle use or wrong answer review button
1565   * @property int $nextpagedefault Override the default next page
1566   * @property int $feedback Toggles display of default feedback
1567   * @property int $minquestions Sets a minimum value of pages seen when calculating grades
1568   * @property int $maxpages Maximum number of pages this lesson can contain
1569   * @property int $retake Flag to allow users to retake a lesson
1570   * @property int $activitylink Relate this lesson to another lesson
1571   * @property string $mediafile File to pop up to or webpage to display
1572   * @property int $mediaheight Sets the height of the media file popup
1573   * @property int $mediawidth Sets the width of the media file popup
1574   * @property int $mediaclose Toggle display of a media close button
1575   * @property int $slideshow Flag for whether branch pages should be shown as slideshows
1576   * @property int $width Width of slideshow
1577   * @property int $height Height of slideshow
1578   * @property string $bgcolor Background colour of slideshow
1579   * @property int $displayleft Display a left menu
1580   * @property int $displayleftif Sets the condition on which the left menu is displayed
1581   * @property int $progressbar Flag to toggle display of a lesson progress bar
1582   * @property int $available Timestamp of when this lesson becomes available
1583   * @property int $deadline Timestamp of when this lesson is no longer available
1584   * @property int $timemodified Timestamp when lesson was last modified
1585   * @property int $allowofflineattempts Whether to allow the lesson to be attempted offline in the mobile app
1586   *
1587   * These properties are calculated
1588   * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
1589   * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
1590   *
1591   * @copyright  2009 Sam Hemelryk
1592   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1593   */
1594  class lesson extends lesson_base {
1595  
1596      /**
1597       * The id of the first page (where prevpageid = 0) gets set and retrieved by
1598       * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
1599       * @var int
1600       */
1601      protected $firstpageid = null;
1602      /**
1603       * The id of the last page (where nextpageid = 0) gets set and retrieved by
1604       * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
1605       * @var int
1606       */
1607      protected $lastpageid = null;
1608      /**
1609       * An array used to cache the pages associated with this lesson after the first
1610       * time they have been loaded.
1611       * A note to developers: If you are going to be working with MORE than one or
1612       * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
1613       * in order to save excess database queries.
1614       * @var array An array of lesson_page objects
1615       */
1616      protected $pages = array();
1617      /**
1618       * Flag that gets set to true once all of the pages associated with the lesson
1619       * have been loaded.
1620       * @var bool
1621       */
1622      protected $loadedallpages = false;
1623  
1624      /**
1625       * Course module object gets set and retrieved by directly calling <code>$lesson->cm;</code>
1626       * @see get_cm()
1627       * @var stdClass
1628       */
1629      protected $cm = null;
1630  
1631      /**
1632       * Course object gets set and retrieved by directly calling <code>$lesson->courserecord;</code>
1633       * @see get_courserecord()
1634       * @var stdClass
1635       */
1636      protected $courserecord = null;
1637  
1638      /**
1639       * Context object gets set and retrieved by directly calling <code>$lesson->context;</code>
1640       * @see get_context()
1641       * @var stdClass
1642       */
1643      protected $context = null;
1644  
1645      /**
1646       * Constructor method
1647       *
1648       * @param object $properties
1649       * @param stdClass $cm course module object
1650       * @param stdClass $course course object
1651       * @since Moodle 3.3
1652       */
1653      public function __construct($properties, $cm = null, $course = null) {
1654          parent::__construct($properties);
1655          $this->cm = $cm;
1656          $this->courserecord = $course;
1657      }
1658  
1659      /**
1660       * Simply generates a lesson object given an array/object of properties
1661       * Overrides {@see lesson_base->create()}
1662       * @static
1663       * @param object|array $properties
1664       * @return lesson
1665       */
1666      public static function create($properties) {
1667          return new lesson($properties);
1668      }
1669  
1670      /**
1671       * Generates a lesson object from the database given its id
1672       * @static
1673       * @param int $lessonid
1674       * @return lesson
1675       */
1676      public static function load($lessonid) {
1677          global $DB;
1678  
1679          if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
1680              print_error('invalidcoursemodule');
1681          }
1682          return new lesson($lesson);
1683      }
1684  
1685      /**
1686       * Deletes this lesson from the database
1687       */
1688      public function delete() {
1689          global $CFG, $DB;
1690          require_once($CFG->libdir.'/gradelib.php');
1691          require_once($CFG->dirroot.'/calendar/lib.php');
1692  
1693          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1694          $context = context_module::instance($cm->id);
1695  
1696          $this->delete_all_overrides();
1697  
1698          grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1));
1699  
1700          // We must delete the module record after we delete the grade item.
1701          $DB->delete_records("lesson", array("id"=>$this->properties->id));
1702          $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
1703          $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
1704          $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
1705          $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
1706          $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
1707          $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
1708          if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
1709              $coursecontext = context_course::instance($cm->course);
1710              foreach($events as $event) {
1711                  $event->context = $coursecontext;
1712                  $event = calendar_event::load($event);
1713                  $event->delete();
1714              }
1715          }
1716  
1717          // Delete files associated with this module.
1718          $fs = get_file_storage();
1719          $fs->delete_area_files($context->id);
1720  
1721          return true;
1722      }
1723  
1724      /**
1725       * Deletes a lesson override from the database and clears any corresponding calendar events
1726       *
1727       * @param int $overrideid The id of the override being deleted
1728       * @return bool true on success
1729       */
1730      public function delete_override($overrideid) {
1731          global $CFG, $DB;
1732  
1733          require_once($CFG->dirroot . '/calendar/lib.php');
1734  
1735          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1736  
1737          $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST);
1738  
1739          // Delete the events.
1740          $conds = array('modulename' => 'lesson',
1741                  'instance' => $this->properties->id);
1742          if (isset($override->userid)) {
1743              $conds['userid'] = $override->userid;
1744              $cachekey = "{$cm->instance}_u_{$override->userid}";
1745          } else {
1746              $conds['groupid'] = $override->groupid;
1747              $cachekey = "{$cm->instance}_g_{$override->groupid}";
1748          }
1749          $events = $DB->get_records('event', $conds);
1750          foreach ($events as $event) {
1751              $eventold = calendar_event::load($event);
1752              $eventold->delete();
1753          }
1754  
1755          $DB->delete_records('lesson_overrides', array('id' => $overrideid));
1756          cache::make('mod_lesson', 'overrides')->delete($cachekey);
1757  
1758          // Set the common parameters for one of the events we will be triggering.
1759          $params = array(
1760              'objectid' => $override->id,
1761              'context' => context_module::instance($cm->id),
1762              'other' => array(
1763                  'lessonid' => $override->lessonid
1764              )
1765          );
1766          // Determine which override deleted event to fire.
1767          if (!empty($override->userid)) {
1768              $params['relateduserid'] = $override->userid;
1769              $event = \mod_lesson\event\user_override_deleted::create($params);
1770          } else {
1771              $params['other']['groupid'] = $override->groupid;
1772              $event = \mod_lesson\event\group_override_deleted::create($params);
1773          }
1774  
1775          // Trigger the override deleted event.
1776          $event->add_record_snapshot('lesson_overrides', $override);
1777          $event->trigger();
1778  
1779          return true;
1780      }
1781  
1782      /**
1783       * Deletes all lesson overrides from the database and clears any corresponding calendar events
1784       */
1785      public function delete_all_overrides() {
1786          global $DB;
1787  
1788          $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id');
1789          foreach ($overrides as $override) {
1790              $this->delete_override($override->id);
1791          }
1792      }
1793  
1794      /**
1795       * Checks user enrollment in the current course.
1796       *
1797       * @param int $userid
1798       * @return null|stdClass user record
1799       */
1800      public function is_participant($userid) {
1801          return is_enrolled($this->get_context(), $userid, 'mod/lesson:view', $this->show_only_active_users());
1802      }
1803  
1804      /**
1805       * Check is only active users in course should be shown.
1806       *
1807       * @return bool true if only active users should be shown.
1808       */
1809      public function show_only_active_users() {
1810          return !has_capability('moodle/course:viewsuspendedusers', $this->get_context());
1811      }
1812  
1813      /**
1814       * Updates the lesson properties with override information for a user.
1815       *
1816       * Algorithm:  For each lesson setting, if there is a matching user-specific override,
1817       *   then use that otherwise, if there are group-specific overrides, return the most
1818       *   lenient combination of them.  If neither applies, leave the quiz setting unchanged.
1819       *
1820       *   Special case: if there is more than one password that applies to the user, then
1821       *   lesson->extrapasswords will contain an array of strings giving the remaining
1822       *   passwords.
1823       *
1824       * @param int $userid The userid.
1825       */
1826      public function update_effective_access($userid) {
1827          global $DB;
1828  
1829          // Check for user override.
1830          $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid));
1831  
1832          if (!$override) {
1833              $override = new stdClass();
1834              $override->available = null;
1835              $override->deadline = null;
1836              $override->timelimit = null;
1837              $override->review = null;
1838              $override->maxattempts = null;
1839              $override->retake = null;
1840              $override->password = null;
1841          }
1842  
1843          // Check for group overrides.
1844          $groupings = groups_get_user_groups($this->properties->course, $userid);
1845  
1846          if (!empty($groupings[0])) {
1847              // Select all overrides that apply to the User's groups.
1848              list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1849              $sql = "SELECT * FROM {lesson_overrides}
1850                      WHERE groupid $extra AND lessonid = ?";
1851              $params[] = $this->properties->id;
1852              $records = $DB->get_records_sql($sql, $params);
1853  
1854              // Combine the overrides.
1855              $availables = array();
1856              $deadlines = array();
1857              $timelimits = array();
1858              $reviews = array();
1859              $attempts = array();
1860              $retakes = array();
1861              $passwords = array();
1862  
1863              foreach ($records as $gpoverride) {
1864                  if (isset($gpoverride->available)) {
1865                      $availables[] = $gpoverride->available;
1866                  }
1867                  if (isset($gpoverride->deadline)) {
1868                      $deadlines[] = $gpoverride->deadline;
1869                  }
1870                  if (isset($gpoverride->timelimit)) {
1871                      $timelimits[] = $gpoverride->timelimit;
1872                  }
1873                  if (isset($gpoverride->review)) {
1874                      $reviews[] = $gpoverride->review;
1875                  }
1876                  if (isset($gpoverride->maxattempts)) {
1877                      $attempts[] = $gpoverride->maxattempts;
1878                  }
1879                  if (isset($gpoverride->retake)) {
1880                      $retakes[] = $gpoverride->retake;
1881                  }
1882                  if (isset($gpoverride->password)) {
1883                      $passwords[] = $gpoverride->password;
1884                  }
1885              }
1886              // If there is a user override for a setting, ignore the group override.
1887              if (is_null($override->available) && count($availables)) {
1888                  $override->available = min($availables);
1889              }
1890              if (is_null($override->deadline) && count($deadlines)) {
1891                  if (in_array(0, $deadlines)) {
1892                      $override->deadline = 0;
1893                  } else {
1894                      $override->deadline = max($deadlines);
1895                  }
1896              }
1897              if (is_null($override->timelimit) && count($timelimits)) {
1898                  if (in_array(0, $timelimits)) {
1899                      $override->timelimit = 0;
1900                  } else {
1901                      $override->timelimit = max($timelimits);
1902                  }
1903              }
1904              if (is_null($override->review) && count($reviews)) {
1905                  $override->review = max($reviews);
1906              }
1907              if (is_null($override->maxattempts) && count($attempts)) {
1908                  $override->maxattempts = max($attempts);
1909              }
1910              if (is_null($override->retake) && count($retakes)) {
1911                  $override->retake = max($retakes);
1912              }
1913              if (is_null($override->password) && count($passwords)) {
1914                  $override->password = array_shift($passwords);
1915                  if (count($passwords)) {
1916                      $override->extrapasswords = $passwords;
1917                  }
1918              }
1919  
1920          }
1921  
1922          // Merge with lesson defaults.
1923          $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake');
1924          foreach ($keys as $key) {
1925              if (isset($override->{$key})) {
1926                  $this->properties->{$key} = $override->{$key};
1927              }
1928          }
1929  
1930          // Special handling of lesson usepassword and password.
1931          if (isset($override->password)) {
1932              if ($override->password == '') {
1933                  $this->properties->usepassword = 0;
1934              } else {
1935                  $this->properties->usepassword = 1;
1936                  $this->properties->password = $override->password;
1937                  if (isset($override->extrapasswords)) {
1938                      $this->properties->extrapasswords = $override->extrapasswords;
1939                  }
1940              }
1941          }
1942      }
1943  
1944      /**
1945       * Fetches messages from the session that may have been set in previous page
1946       * actions.
1947       *
1948       * <code>
1949       * // Do not call this method directly instead use
1950       * $lesson->messages;
1951       * </code>
1952       *
1953       * @return array
1954       */
1955      protected function get_messages() {
1956          global $SESSION;
1957  
1958          $messages = array();
1959          if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1960              $messages = $SESSION->lesson_messages[$this->properties->id];
1961              unset($SESSION->lesson_messages[$this->properties->id]);
1962          }
1963  
1964          return $messages;
1965      }
1966  
1967      /**
1968       * Get all of the attempts for the current user.
1969       *
1970       * @param int $retries
1971       * @param bool $correct Optional: only fetch correct attempts
1972       * @param int $pageid Optional: only fetch attempts at the given page
1973       * @param int $userid Optional: defaults to the current user if not set
1974       * @return array|false
1975       */
1976      public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1977          global $USER, $DB;
1978          $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1979          if ($correct) {
1980              $params['correct'] = 1;
1981          }
1982          if ($pageid !== null) {
1983              $params['pageid'] = $pageid;
1984          }
1985          if ($userid === null) {
1986              $params['userid'] = $USER->id;
1987          }
1988          return $DB->get_records('lesson_attempts', $params, 'timeseen ASC');
1989      }
1990  
1991  
1992      /**
1993       * Get a list of content pages (formerly known as branch tables) viewed in the lesson for the given user during an attempt.
1994       *
1995       * @param  int $lessonattempt the lesson attempt number (also known as retries)
1996       * @param  int $userid        the user id to retrieve the data from
1997       * @param  string $sort          an order to sort the results in (a valid SQL ORDER BY parameter)
1998       * @param  string $fields        a comma separated list of fields to return
1999       * @return array of pages
2000       * @since  Moodle 3.3
2001       */
2002      public function get_content_pages_viewed($lessonattempt, $userid = null, $sort = '', $fields = '*') {
2003          global $USER, $DB;
2004  
2005          if ($userid === null) {
2006              $userid = $USER->id;
2007          }
2008          $conditions = array("lessonid" => $this->properties->id, "userid" => $userid, "retry" => $lessonattempt);
2009          return $DB->get_records('lesson_branch', $conditions, $sort, $fields);
2010      }
2011  
2012      /**
2013       * Returns the first page for the lesson or false if there isn't one.
2014       *
2015       * This method should be called via the magic method __get();
2016       * <code>
2017       * $firstpage = $lesson->firstpage;
2018       * </code>
2019       *
2020       * @return lesson_page|bool Returns the lesson_page specialised object or false
2021       */
2022      protected function get_firstpage() {
2023          $pages = $this->load_all_pages();
2024          if (count($pages) > 0) {
2025              foreach ($pages as $page) {
2026                  if ((int)$page->prevpageid === 0) {
2027                      return $page;
2028                  }
2029              }
2030          }
2031          return false;
2032      }
2033  
2034      /**
2035       * Returns the last page for the lesson or false if there isn't one.
2036       *
2037       * This method should be called via the magic method __get();
2038       * <code>
2039       * $lastpage = $lesson->lastpage;
2040       * </code>
2041       *
2042       * @return lesson_page|bool Returns the lesson_page specialised object or false
2043       */
2044      protected function get_lastpage() {
2045          $pages = $this->load_all_pages();
2046          if (count($pages) > 0) {
2047              foreach ($pages as $page) {
2048                  if ((int)$page->nextpageid === 0) {
2049                      return $page;
2050                  }
2051              }
2052          }
2053          return false;
2054      }
2055  
2056      /**
2057       * Returns the id of the first page of this lesson. (prevpageid = 0)
2058       * @return int
2059       */
2060      protected function get_firstpageid() {
2061          global $DB;
2062          if ($this->firstpageid == null) {
2063              if (!$this->loadedallpages) {
2064                  $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
2065                  if (!$firstpageid) {
2066                      print_error('cannotfindfirstpage', 'lesson');
2067                  }
2068                  $this->firstpageid = $firstpageid;
2069              } else {
2070                  $firstpage = $this->get_firstpage();
2071                  $this->firstpageid = $firstpage->id;
2072              }
2073          }
2074          return $this->firstpageid;
2075      }
2076  
2077      /**
2078       * Returns the id of the last page of this lesson. (nextpageid = 0)
2079       * @return int
2080       */
2081      public function get_lastpageid() {
2082          global $DB;
2083          if ($this->lastpageid == null) {
2084              if (!$this->loadedallpages) {
2085                  $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
2086                  if (!$lastpageid) {
2087                      print_error('cannotfindlastpage', 'lesson');
2088                  }
2089                  $this->lastpageid = $lastpageid;
2090              } else {
2091                  $lastpageid = $this->get_lastpage();
2092                  $this->lastpageid = $lastpageid->id;
2093              }
2094          }
2095  
2096          return $this->lastpageid;
2097      }
2098  
2099       /**
2100       * Gets the next page id to display after the one that is provided.
2101       * @param int $nextpageid
2102       * @return bool
2103       */
2104      public function get_next_page($nextpageid) {
2105          global $USER, $DB;
2106          $allpages = $this->load_all_pages();
2107          if ($this->properties->nextpagedefault) {
2108              // in Flash Card mode...first get number of retakes
2109              $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
2110              shuffle($allpages);
2111              $found = false;
2112              if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
2113                  foreach ($allpages as $nextpage) {
2114                      if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) {
2115                          $found = true;
2116                          break;
2117                      }
2118                  }
2119              } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
2120                  foreach ($allpages as $nextpage) {
2121                      if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) {
2122                          $found = true;
2123                          break;
2124                      }
2125                  }
2126              }
2127              if ($found) {
2128                  if ($this->properties->maxpages) {
2129                      // check number of pages viewed (in the lesson)
2130                      if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) {
2131                          return LESSON_EOL;
2132                      }
2133                  }
2134                  return $nextpage->id;
2135              }
2136          }
2137          // In a normal lesson mode
2138          foreach ($allpages as $nextpage) {
2139              if ((int)$nextpage->id === (int)$nextpageid) {
2140                  return $nextpage->id;
2141              }
2142          }
2143          return LESSON_EOL;
2144      }
2145  
2146      /**
2147       * Sets a message against the session for this lesson that will displayed next
2148       * time the lesson processes messages
2149       *
2150       * @param string $message
2151       * @param string $class
2152       * @param string $align
2153       * @return bool
2154       */
2155      public function add_message($message, $class="notifyproblem", $align='center') {
2156          global $SESSION;
2157  
2158          if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
2159              $SESSION->lesson_messages = array();
2160              $SESSION->lesson_messages[$this->properties->id] = array();
2161          } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
2162              $SESSION->lesson_messages[$this->properties->id] = array();
2163          }
2164  
2165          $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
2166  
2167          return true;
2168      }
2169  
2170      /**
2171       * Check if the lesson is accessible at the present time
2172       * @return bool True if the lesson is accessible, false otherwise
2173       */
2174      public function is_accessible() {
2175          $available = $this->properties->available;
2176          $deadline = $this->properties->deadline;
2177          return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
2178      }
2179  
2180      /**
2181       * Starts the lesson time for the current user
2182       * @return bool Returns true
2183       */
2184      public function start_timer() {
2185          global $USER, $DB;
2186  
2187          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2188              false, MUST_EXIST);
2189  
2190          // Trigger lesson started event.
2191          $event = \mod_lesson\event\lesson_started::create(array(
2192              'objectid' => $this->properties()->id,
2193              'context' => context_module::instance($cm->id),
2194              'courseid' => $this->properties()->course
2195          ));
2196          $event->trigger();
2197  
2198          $USER->startlesson[$this->properties->id] = true;
2199  
2200          $timenow = time();
2201          $startlesson = new stdClass;
2202          $startlesson->lessonid = $this->properties->id;
2203          $startlesson->userid = $USER->id;
2204          $startlesson->starttime = $timenow;
2205          $startlesson->lessontime = $timenow;
2206          if (WS_SERVER) {
2207              $startlesson->timemodifiedoffline = $timenow;
2208          }
2209          $DB->insert_record('lesson_timer', $startlesson);
2210          if ($this->properties->timelimit) {
2211              $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center');
2212          }
2213          return true;
2214      }
2215  
2216      /**
2217       * Updates the timer to the current time and returns the new timer object
2218       * @param bool $restart If set to true the timer is restarted
2219       * @param bool $continue If set to true AND $restart=true then the timer
2220       *                        will continue from a previous attempt
2221       * @return stdClass The new timer
2222       */
2223      public function update_timer($restart=false, $continue=false, $endreached =false) {
2224          global $USER, $DB;
2225  
2226          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2227  
2228          // clock code
2229          // get time information for this user
2230          if (!$timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) {
2231              $this->start_timer();
2232              $timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1);
2233          }
2234          $timer = current($timer); // This will get the latest start time record.
2235  
2236          if ($restart) {
2237              if ($continue) {
2238                  // continue a previous test, need to update the clock  (think this option is disabled atm)
2239                  $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
2240  
2241                  // Trigger lesson resumed event.
2242                  $event = \mod_lesson\event\lesson_resumed::create(array(
2243                      'objectid' => $this->properties->id,
2244                      'context' => context_module::instance($cm->id),
2245                      'courseid' => $this->properties->course
2246                  ));
2247                  $event->trigger();
2248  
2249              } else {
2250                  // starting over, so reset the clock
2251                  $timer->starttime = time();
2252  
2253                  // Trigger lesson restarted event.
2254                  $event = \mod_lesson\event\lesson_restarted::create(array(
2255                      'objectid' => $this->properties->id,
2256                      'context' => context_module::instance($cm->id),
2257                      'courseid' => $this->properties->course
2258                  ));
2259                  $event->trigger();
2260  
2261              }
2262          }
2263  
2264          $timenow = time();
2265          $timer->lessontime = $timenow;
2266          if (WS_SERVER) {
2267              $timer->timemodifiedoffline = $timenow;
2268          }
2269          $timer->completed = $endreached;
2270          $DB->update_record('lesson_timer', $timer);
2271  
2272          // Update completion state.
2273          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2274              false, MUST_EXIST);
2275          $course = get_course($cm->course);
2276          $completion = new completion_info($course);
2277          if ($completion->is_enabled($cm) && $this->properties()->completiontimespent > 0) {
2278              $completion->update_state($cm, COMPLETION_COMPLETE);
2279          }
2280          return $timer;
2281      }
2282  
2283      /**
2284       * Updates the timer to the current time then stops it by unsetting the user var
2285       * @return bool Returns true
2286       */
2287      public function stop_timer() {
2288          global $USER, $DB;
2289          unset($USER->startlesson[$this->properties->id]);
2290  
2291          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2292              false, MUST_EXIST);
2293  
2294          // Trigger lesson ended event.
2295          $event = \mod_lesson\event\lesson_ended::create(array(
2296              'objectid' => $this->properties()->id,
2297              'context' => context_module::instance($cm->id),
2298              'courseid' => $this->properties()->course
2299          ));
2300          $event->trigger();
2301  
2302          return $this->update_timer(false, false, true);
2303      }
2304  
2305      /**
2306       * Checks to see if the lesson has pages
2307       */
2308      public function has_pages() {
2309          global $DB;
2310          $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
2311          return ($pagecount>0);
2312      }
2313  
2314      /**
2315       * Returns the link for the related activity
2316       * @return string
2317       */
2318      public function link_for_activitylink() {
2319          global $DB;
2320          $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
2321          if ($module) {
2322              $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
2323              if ($modname) {
2324                  $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
2325                  if ($instancename) {
2326                      return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php',
2327                          array('id' => $this->properties->activitylink)), get_string('activitylinkname',
2328                          'lesson', $instancename), array('class' => 'centerpadded lessonbutton standardbutton pr-3'));
2329                  }
2330              }
2331          }
2332          return '';
2333      }
2334  
2335      /**
2336       * Loads the requested page.
2337       *
2338       * This function will return the requested page id as either a specialised
2339       * lesson_page object OR as a generic lesson_page.
2340       * If the page has been loaded previously it will be returned from the pages
2341       * array, otherwise it will be loaded from the database first
2342       *
2343       * @param int $pageid
2344       * @return lesson_page A lesson_page object or an object that extends it
2345       */
2346      public function load_page($pageid) {
2347          if (!array_key_exists($pageid, $this->pages)) {
2348              $manager = lesson_page_type_manager::get($this);
2349              $this->pages[$pageid] = $manager->load_page($pageid, $this);
2350          }
2351          return $this->pages[$pageid];
2352      }
2353  
2354      /**
2355       * Loads ALL of the pages for this lesson
2356       *
2357       * @return array An array containing all pages from this lesson
2358       */
2359      public function load_all_pages() {
2360          if (!$this->loadedallpages) {
2361              $manager = lesson_page_type_manager::get($this);
2362              $this->pages = $manager->load_all_pages($this);
2363              $this->loadedallpages = true;
2364          }
2365          return $this->pages;
2366      }
2367  
2368      /**
2369       * Duplicate the lesson page.
2370       *
2371       * @param  int $pageid Page ID of the page to duplicate.
2372       * @return void.
2373       */
2374      public function duplicate_page($pageid) {
2375          global $PAGE;
2376          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2377          $context = context_module::instance($cm->id);
2378          // Load the page.
2379          $page = $this->load_page($pageid);
2380          $properties = $page->properties();
2381          // The create method checks to see if these properties are set and if not sets them to zero, hence the unsetting here.
2382          if (!$properties->qoption) {
2383              unset($properties->qoption);
2384          }
2385          if (!$properties->layout) {
2386              unset($properties->layout);
2387          }
2388          if (!$properties->display) {
2389              unset($properties->display);
2390          }
2391  
2392          $properties->pageid = $pageid;
2393          // Add text and format into the format required to create a new page.
2394          $properties->contents_editor = array(
2395              'text' => $properties->contents,
2396              'format' => $properties->contentsformat
2397          );
2398          $answers = $page->get_answers();
2399          // Answers need to be added to $properties.
2400          $i = 0;
2401          $answerids = array();
2402          foreach ($answers as $answer) {
2403              // Needs to be rearranged to work with the create function.
2404              $properties->answer_editor[$i] = array(
2405                  'text' => $answer->answer,
2406                  'format' => $answer->answerformat
2407              );
2408  
2409              $properties->response_editor[$i] = array(
2410                'text' => $answer->response,
2411                'format' => $answer->responseformat
2412              );
2413              $answerids[] = $answer->id;
2414  
2415              $properties->jumpto[$i] = $answer->jumpto;
2416              $properties->score[$i] = $answer->score;
2417  
2418              $i++;
2419          }
2420          // Create the duplicate page.
2421          $newlessonpage = lesson_page::create($properties, $this, $context, $PAGE->course->maxbytes);
2422          $newanswers = $newlessonpage->get_answers();
2423          // Copy over the file areas as well.
2424          $this->copy_page_files('page_contents', $pageid, $newlessonpage->id, $context->id);
2425          $j = 0;
2426          foreach ($newanswers as $answer) {
2427              if (isset($answer->answer) && strpos($answer->answer, '@@PLUGINFILE@@') !== false) {
2428                  $this->copy_page_files('page_answers', $answerids[$j], $answer->id, $context->id);
2429              }
2430              if (isset($answer->response) && !is_array($answer->response) && strpos($answer->response, '@@PLUGINFILE@@') !== false) {
2431                  $this->copy_page_files('page_responses', $answerids[$j], $answer->id, $context->id);
2432              }
2433              $j++;
2434          }
2435      }
2436  
2437      /**
2438       * Copy the files from one page to another.
2439       *
2440       * @param  string $filearea Area that the files are stored.
2441       * @param  int $itemid Item ID.
2442       * @param  int $newitemid The item ID for the new page.
2443       * @param  int $contextid Context ID for this page.
2444       * @return void.
2445       */
2446      protected function copy_page_files($filearea, $itemid, $newitemid, $contextid) {
2447          $fs = get_file_storage();
2448          $files = $fs->get_area_files($contextid, 'mod_lesson', $filearea, $itemid);
2449          foreach ($files as $file) {
2450              $fieldupdates = array('itemid' => $newitemid);
2451              $fs->create_file_from_storedfile($fieldupdates, $file);
2452          }
2453      }
2454  
2455      /**
2456       * Determines if a jumpto value is correct or not.
2457       *
2458       * returns true if jumpto page is (logically) after the pageid page or
2459       * if the jumpto value is a special value.  Returns false in all other cases.
2460       *
2461       * @param int $pageid Id of the page from which you are jumping from.
2462       * @param int $jumpto The jumpto number.
2463       * @return boolean True or false after a series of tests.
2464       **/
2465      public function jumpto_is_correct($pageid, $jumpto) {
2466          global $DB;
2467  
2468          // first test the special values
2469          if (!$jumpto) {
2470              // same page
2471              return false;
2472          } elseif ($jumpto == LESSON_NEXTPAGE) {
2473              return true;
2474          } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
2475              return true;
2476          } elseif ($jumpto == LESSON_RANDOMPAGE) {
2477              return true;
2478          } elseif ($jumpto == LESSON_CLUSTERJUMP) {
2479              return true;
2480          } elseif ($jumpto == LESSON_EOL) {
2481              return true;
2482          }
2483  
2484          $pages = $this->load_all_pages();
2485          $apageid = $pages[$pageid]->nextpageid;
2486          while ($apageid != 0) {
2487              if ($jumpto == $apageid) {
2488                  return true;
2489              }
2490              $apageid = $pages[$apageid]->nextpageid;
2491          }
2492          return false;
2493      }
2494  
2495      /**
2496       * Returns the time a user has remaining on this lesson
2497       * @param int $starttime Starttime timestamp
2498       * @return string
2499       */
2500      public function time_remaining($starttime) {
2501          $timeleft = $starttime + $this->properties->timelimit - time();
2502          $hours = floor($timeleft/3600);
2503          $timeleft = $timeleft - ($hours * 3600);
2504          $minutes = floor($timeleft/60);
2505          $secs = $timeleft - ($minutes * 60);
2506  
2507          if ($minutes < 10) {
2508              $minutes = "0$minutes";
2509          }
2510          if ($secs < 10) {
2511              $secs = "0$secs";
2512          }
2513          $output   = array();
2514          $output[] = $hours;
2515          $output[] = $minutes;
2516          $output[] = $secs;
2517          $output = implode(':', $output);
2518          return $output;
2519      }
2520  
2521      /**
2522       * Interprets LESSON_CLUSTERJUMP jumpto value.
2523       *
2524       * This will select a page randomly
2525       * and the page selected will be inbetween a cluster page and end of clutter or end of lesson
2526       * and the page selected will be a page that has not been viewed already
2527       * and if any pages are within a branch table or end of branch then only 1 page within
2528       * the branch table or end of branch will be randomly selected (sub clustering).
2529       *
2530       * @param int $pageid Id of the current page from which we are jumping from.
2531       * @param int $userid Id of the user.
2532       * @return int The id of the next page.
2533       **/
2534      public function cluster_jump($pageid, $userid=null) {
2535          global $DB, $USER;
2536  
2537          if ($userid===null) {
2538              $userid = $USER->id;
2539          }
2540          // get the number of retakes
2541          if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
2542              $retakes = 0;
2543          }
2544          // get all the lesson_attempts aka what the user has seen
2545          $seenpages = array();
2546          if ($attempts = $this->get_attempts($retakes)) {
2547              foreach ($attempts as $attempt) {
2548                  $seenpages[$attempt->pageid] = $attempt->pageid;
2549              }
2550  
2551          }
2552  
2553          // get the lesson pages
2554          $lessonpages = $this->load_all_pages();
2555          // find the start of the cluster
2556          while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
2557              if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
2558                  break;
2559              }
2560              $pageid = $lessonpages[$pageid]->prevpageid;
2561          }
2562  
2563          $clusterpages = array();
2564          $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
2565          $unseen = array();
2566          foreach ($clusterpages as $key=>$cluster) {
2567              // Remove the page if  it is in a branch table or is an endofbranch.
2568              if ($this->is_sub_page_of_type($cluster->id,
2569                      array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))
2570                      || $cluster->qtype == LESSON_PAGE_ENDOFBRANCH) {
2571                  unset($clusterpages[$key]);
2572              } else if ($cluster->qtype == LESSON_PAGE_BRANCHTABLE) {
2573                  // If branchtable, check to see if any pages inside have been viewed.
2574                  $branchpages = $this->get_sub_pages_of($cluster->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
2575                  $flag = true;
2576                  foreach ($branchpages as $branchpage) {
2577                      if (array_key_exists($branchpage->id, $seenpages)) {  // Check if any of the pages have been viewed.
2578                          $flag = false;
2579                      }
2580                  }
2581                  if ($flag && count($branchpages) > 0) {
2582                      // Add branch table.
2583                      $unseen[] = $cluster;
2584                  }
2585              } elseif ($cluster->is_unseen($seenpages)) {
2586                  $unseen[] = $cluster;
2587              }
2588          }
2589  
2590          if (count($unseen) > 0) {
2591              // it does not contain elements, then use exitjump, otherwise find out next page/branch
2592              $nextpage = $unseen[rand(0, count($unseen)-1)];
2593              if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
2594                  // if branch table, then pick a random page inside of it
2595                  $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
2596                  return $branchpages[rand(0, count($branchpages)-1)]->id;
2597              } else { // otherwise, return the page's id
2598                  return $nextpage->id;
2599              }
2600          } else {
2601              // seen all there is to see, leave the cluster
2602              if (end($clusterpages)->nextpageid == 0) {
2603                  return LESSON_EOL;
2604              } else {
2605                  $clusterendid = $pageid;
2606                  while ($clusterendid != 0) { // This condition should not be satisfied... should be an end of cluster page.
2607                      if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_ENDOFCLUSTER) {
2608                          break;
2609                      }
2610                      $clusterendid = $lessonpages[$clusterendid]->nextpageid;
2611                  }
2612                  $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
2613                  if ($exitjump == LESSON_NEXTPAGE) {
2614                      $exitjump = $lessonpages[$clusterendid]->nextpageid;
2615                  }
2616                  if ($exitjump == 0) {
2617                      return LESSON_EOL;
2618                  } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
2619                      return $exitjump;
2620                  } else {
2621                      if (!array_key_exists($exitjump, $lessonpages)) {
2622                          $found = false;
2623                          foreach ($lessonpages as $page) {
2624                              if ($page->id === $clusterendid) {
2625                                  $found = true;
2626                              } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
2627                                  $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
2628                                  if ($exitjump == LESSON_NEXTPAGE) {
2629                                      $exitjump = $lessonpages[$page->id]->nextpageid;
2630                                  }
2631                                  break;
2632                              }
2633                          }
2634                      }
2635                      if (!array_key_exists($exitjump, $lessonpages)) {
2636                          return LESSON_EOL;
2637                      }
2638                      // Check to see that the return type is not a cluster.
2639                      if ($lessonpages[$exitjump]->qtype == LESSON_PAGE_CLUSTER) {
2640                          // If the exitjump is a cluster then go through this function again and try to find an unseen question.
2641                          $exitjump = $this->cluster_jump($exitjump, $userid);
2642                      }
2643                      return $exitjump;
2644                  }
2645              }
2646          }
2647      }
2648  
2649      /**
2650       * Finds all pages that appear to be a subtype of the provided pageid until
2651       * an end point specified within $ends is encountered or no more pages exist
2652       *
2653       * @param int $pageid
2654       * @param array $ends An array of LESSON_PAGE_* types that signify an end of
2655       *               the subtype
2656       * @return array An array of specialised lesson_page objects
2657       */
2658      public function get_sub_pages_of($pageid, array $ends) {
2659          $lessonpages = $this->load_all_pages();
2660          $pageid = $lessonpages[$pageid]->nextpageid;  // move to the first page after the branch table
2661          $pages = array();
2662  
2663          while (true) {
2664              if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
2665                  break;
2666              }
2667              $pages[] = $lessonpages[$pageid];
2668              $pageid = $lessonpages[$pageid]->nextpageid;
2669          }
2670  
2671          return $pages;
2672      }
2673  
2674      /**
2675       * Checks to see if the specified page[id] is a subpage of a type specified in
2676       * the $types array, until either there are no more pages of we find a type
2677       * corresponding to that of a type specified in $ends
2678       *
2679       * @param int $pageid The id of the page to check
2680       * @param array $types An array of types that would signify this page was a subpage
2681       * @param array $ends An array of types that mean this is not a subpage
2682       * @return bool
2683       */
2684      public function is_sub_page_of_type($pageid, array $types, array $ends) {
2685          $pages = $this->load_all_pages();
2686          $pageid = $pages[$pageid]->prevpageid; // move up one
2687  
2688          array_unshift($ends, 0);
2689          // go up the pages till branch table
2690          while (true) {
2691              if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
2692                  return false;
2693              } else if (in_array($pages[$pageid]->qtype, $types)) {
2694                  return true;
2695              }
2696              $pageid = $pages[$pageid]->prevpageid;
2697          }
2698      }
2699  
2700      /**
2701       * Move a page resorting all other pages.
2702       *
2703       * @param int $pageid
2704       * @param int $after
2705       * @return void
2706       */
2707      public function resort_pages($pageid, $after) {
2708          global $CFG;
2709  
2710          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2711          $context = context_module::instance($cm->id);
2712  
2713          $pages = $this->load_all_pages();
2714  
2715          if (!array_key_exists($pageid, $pages) || ($after!=0 && !array_key_exists($after, $pages))) {
2716              print_error('cannotfindpages', 'lesson', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id");
2717          }
2718  
2719          $pagetomove = clone($pages[$pageid]);
2720          unset($pages[$pageid]);
2721  
2722          $pageids = array();
2723          if ($after === 0) {
2724              $pageids['p0'] = $pageid;
2725          }
2726          foreach ($pages as $page) {
2727              $pageids[] = $page->id;
2728              if ($page->id == $after) {
2729                  $pageids[] = $pageid;
2730              }
2731          }
2732  
2733          $pageidsref = $pageids;
2734          reset($pageidsref);
2735          $prev = 0;
2736          $next = next($pageidsref);
2737          foreach ($pageids as $pid) {
2738              if ($pid === $pageid) {
2739                  $page = $pagetomove;
2740              } else {
2741                  $page = $pages[$pid];
2742              }
2743              if ($page->prevpageid != $prev || $page->nextpageid != $next) {
2744                  $page->move($next, $prev);
2745  
2746                  if ($pid === $pageid) {
2747                      // We will trigger an event.
2748                      $pageupdated = array('next' => $next, 'prev' => $prev);
2749                  }
2750              }
2751  
2752              $prev = $page->id;
2753              $next = next($pageidsref);
2754              if (!$next) {
2755                  $next = 0;
2756              }
2757          }
2758  
2759          // Trigger an event: page moved.
2760          if (!empty($pageupdated)) {
2761              $eventparams = array(
2762                  'context' => $context,
2763                  'objectid' => $pageid,
2764                  'other' => array(
2765                      'pagetype' => $page->get_typestring(),
2766                      'prevpageid' => $pageupdated['prev'],
2767                      'nextpageid' => $pageupdated['next']
2768                  )
2769              );
2770              $event = \mod_lesson\event\page_moved::create($eventparams);
2771              $event->trigger();
2772          }
2773  
2774      }
2775  
2776      /**
2777       * Return the lesson context object.
2778       *
2779       * @return stdClass context
2780       * @since  Moodle 3.3
2781       */
2782      public function get_context() {
2783          if ($this->context == null) {
2784              $this->context = context_module::instance($this->get_cm()->id);
2785          }
2786          return $this->context;
2787      }
2788  
2789      /**
2790       * Set the lesson course module object.
2791       *
2792       * @param stdClass $cm course module objct
2793       * @since  Moodle 3.3
2794       */
2795      private function set_cm($cm) {
2796          $this->cm = $cm;
2797      }
2798  
2799      /**
2800       * Return the lesson course module object.
2801       *
2802       * @return stdClass course module
2803       * @since  Moodle 3.3
2804       */
2805      public function get_cm() {
2806          if ($this->cm == null) {
2807              $this->cm = get_coursemodule_from_instance('lesson', $this->properties->id);
2808          }
2809          return $this->cm;
2810      }
2811  
2812      /**
2813       * Set the lesson course object.
2814       *
2815       * @param stdClass $course course objct
2816       * @since  Moodle 3.3
2817       */
2818      private function set_courserecord($course) {
2819          $this->courserecord = $course;
2820      }
2821  
2822      /**
2823       * Return the lesson course object.
2824       *
2825       * @return stdClass course
2826       * @since  Moodle 3.3
2827       */
2828      public function get_courserecord() {
2829          global $DB;
2830  
2831          if ($this->courserecord == null) {
2832              $this->courserecord = $DB->get_record('course', array('id' => $this->properties->course));
2833          }
2834          return $this->courserecord;
2835      }
2836  
2837      /**
2838       * Check if the user can manage the lesson activity.
2839       *
2840       * @return bool true if the user can manage the lesson
2841       * @since  Moodle 3.3
2842       */
2843      public function can_manage() {
2844          return has_capability('mod/lesson:manage', $this->get_context());
2845      }
2846  
2847      /**
2848       * Check if time restriction is applied.
2849       *
2850       * @return mixed false if  there aren't restrictions or an object with the restriction information
2851       * @since  Moodle 3.3
2852       */
2853      public function get_time_restriction_status() {
2854          if ($this->can_manage()) {
2855              return false;
2856          }
2857  
2858          if (!$this->is_accessible()) {
2859              if ($this->properties->deadline != 0 && time() > $this->properties->deadline) {
2860                  $status = ['reason' => 'lessonclosed', 'time' => $this->properties->deadline];
2861              } else {
2862                  $status = ['reason' => 'lessonopen', 'time' => $this->properties->available];
2863              }
2864              return (object) $status;
2865          }
2866          return false;
2867      }
2868  
2869      /**
2870       * Check if password restriction is applied.
2871       *
2872       * @param string $userpassword the user password to check (if the restriction is set)
2873       * @return mixed false if there aren't restrictions or an object with the restriction information
2874       * @since  Moodle 3.3
2875       */
2876      public function get_password_restriction_status($userpassword) {
2877          global $USER;
2878          if ($this->can_manage()) {
2879              return false;
2880          }
2881  
2882          if ($this->properties->usepassword && empty($USER->lessonloggedin[$this->id])) {
2883              $correctpass = false;
2884              if (!empty($userpassword) &&
2885                      (($this->properties->password == md5(trim($userpassword))) || ($this->properties->password == trim($userpassword)))) {
2886                  // With or without md5 for backward compatibility (MDL-11090).
2887                  $correctpass = true;
2888                  $USER->lessonloggedin[$this->id] = true;
2889              } else if (isset($this->properties->extrapasswords)) {
2890                  // Group overrides may have additional passwords.
2891                  foreach ($this->properties->extrapasswords as $password) {
2892                      if (strcmp($password, md5(trim($userpassword))) === 0 || strcmp($password, trim($userpassword)) === 0) {
2893                          $correctpass = true;
2894                          $USER->lessonloggedin[$this->id] = true;
2895                      }
2896                  }
2897              }
2898              return !$correctpass;
2899          }
2900          return false;
2901      }
2902  
2903      /**
2904       * Check if dependencies restrictions are applied.
2905       *
2906       * @return mixed false if there aren't restrictions or an object with the restriction information
2907       * @since  Moodle 3.3
2908       */
2909      public function get_dependencies_restriction_status() {
2910          global $DB, $USER;
2911          if ($this->can_manage()) {
2912              return false;
2913          }
2914  
2915          if ($dependentlesson = $DB->get_record('lesson', array('id' => $this->properties->dependency))) {
2916              // Lesson exists, so we can proceed.
2917              $conditions = unserialize_object($this->properties->conditions);
2918              // Assume false for all.
2919              $errors = array();
2920              // Check for the timespent condition.
2921              if (!empty($conditions->timespent)) {
2922                  $timespent = false;
2923                  if ($attempttimes = $DB->get_records('lesson_timer', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
2924                      // Go through all the times and test to see if any of them satisfy the condition.
2925                      foreach ($attempttimes as $attempttime) {
2926                          $duration = $attempttime->lessontime - $attempttime->starttime;
2927                          if ($conditions->timespent < $duration / 60) {
2928                              $timespent = true;
2929                          }
2930                      }
2931                  }
2932                  if (!$timespent) {
2933                      $errors[] = get_string('timespenterror', 'lesson', $conditions->timespent);
2934                  }
2935              }
2936              // Check for the gradebetterthan condition.
2937              if (!empty($conditions->gradebetterthan)) {
2938                  $gradebetterthan = false;
2939                  if ($studentgrades = $DB->get_records('lesson_grades', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
2940                      // Go through all the grades and test to see if any of them satisfy the condition.
2941                      foreach ($studentgrades as $studentgrade) {
2942                          if ($studentgrade->grade >= $conditions->gradebetterthan) {
2943                              $gradebetterthan = true;
2944                          }
2945                      }
2946                  }
2947                  if (!$gradebetterthan) {
2948                      $errors[] = get_string('gradebetterthanerror', 'lesson', $conditions->gradebetterthan);
2949                  }
2950              }
2951              // Check for the completed condition.
2952              if (!empty($conditions->completed)) {
2953                  if (!$DB->count_records('lesson_grades', array('userid' => $USER->id, 'lessonid' => $dependentlesson->id))) {
2954                      $errors[] = get_string('completederror', 'lesson');
2955                  }
2956              }
2957              if (!empty($errors)) {
2958                  return (object) ['errors' => $errors, 'dependentlesson' => $dependentlesson];
2959              }
2960          }
2961          return false;
2962      }
2963  
2964      /**
2965       * Check if the lesson is in review mode. (The user already finished it and retakes are not allowed).
2966       *
2967       * @return bool true if is in review mode
2968       * @since  Moodle 3.3
2969       */
2970      public function is_in_review_mode() {
2971          global $DB, $USER;
2972  
2973          $userhasgrade = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
2974          if ($userhasgrade && !$this->properties->retake) {
2975              return true;
2976          }
2977          return false;
2978      }
2979  
2980      /**
2981       * Return the last page the current user saw.
2982       *
2983       * @param int $retriescount the number of retries for the lesson (the last retry number).
2984       * @return mixed false if the user didn't see the lesson or the last page id
2985       */
2986      public function get_last_page_seen($retriescount) {
2987          global $DB, $USER;
2988  
2989          $lastpageseen = false;
2990          $allattempts = $this->get_attempts($retriescount);
2991          if (!empty($allattempts)) {
2992              $attempt = end($allattempts);
2993              $attemptpage = $this->load_page($attempt->pageid);
2994              $jumpto = $DB->get_field('lesson_answers', 'jumpto', array('id' => $attempt->answerid));
2995              // Convert the jumpto to a proper page id.
2996              if ($jumpto == 0) {
2997                  // Check if a question has been incorrectly answered AND no more attempts at it are left.
2998                  $nattempts = $this->get_attempts($attempt->retry, false, $attempt->pageid, $USER->id);
2999                  if (count($nattempts) >= $this->properties->maxattempts) {
3000                      $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
3001                  } else {
3002                      $lastpageseen = $attempt->pageid;
3003                  }
3004              } else if ($jumpto == LESSON_NEXTPAGE) {
3005                  $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
3006              } else if ($jumpto == LESSON_CLUSTERJUMP) {
3007                  $lastpageseen = $this->cluster_jump($attempt->pageid);
3008              } else {
3009                  $lastpageseen = $jumpto;
3010              }
3011          }
3012  
3013          if ($branchtables = $this->get_content_pages_viewed($retriescount, $USER->id, 'timeseen DESC')) {
3014              // In here, user has viewed a branch table.
3015              $lastbranchtable = current($branchtables);
3016              if (count($allattempts) > 0) {
3017                  if ($lastbranchtable->timeseen > $attempt->timeseen) {
3018                      // This branch table was viewed more recently than the question page.
3019                      if (!empty($lastbranchtable->nextpageid)) {
3020                          $lastpageseen = $lastbranchtable->nextpageid;
3021                      } else {
3022                          // Next page ID did not exist prior to MDL-34006.
3023                          $lastpageseen = $lastbranchtable->pageid;
3024                      }
3025                  }
3026              } else {
3027                  // Has not answered any questions but has viewed a branch table.
3028                  if (!empty($lastbranchtable->nextpageid)) {
3029                      $lastpageseen = $lastbranchtable->nextpageid;
3030                  } else {
3031                      // Next page ID did not exist prior to MDL-34006.
3032                      $lastpageseen = $lastbranchtable->pageid;
3033                  }
3034              }
3035          }
3036          return $lastpageseen;
3037      }
3038  
3039      /**
3040       * Return the number of retries in a lesson for a given user.
3041       *
3042       * @param  int $userid the user id
3043       * @return int the retries count
3044       * @since  Moodle 3.3
3045       */
3046      public function count_user_retries($userid) {
3047          global $DB;
3048  
3049          return $DB->count_records('lesson_grades', array("lessonid" => $this->properties->id, "userid" => $userid));
3050      }
3051  
3052      /**
3053       * Check if a user left a timed session.
3054       *
3055       * @param int $retriescount the number of retries for the lesson (the last retry number).
3056       * @return true if the user left the timed session
3057       * @since  Moodle 3.3
3058       */
3059      public function left_during_timed_session($retriescount) {
3060          global $DB, $USER;
3061  
3062          $conditions = array('lessonid' => $this->properties->id, 'userid' => $USER->id, 'retry' => $retriescount);
3063          return $DB->count_records('lesson_attempts', $conditions) > 0 || $DB->count_records('lesson_branch', $conditions) > 0;
3064      }
3065  
3066      /**
3067       * Trigger module viewed event and set the module viewed for completion.
3068       *
3069       * @since  Moodle 3.3
3070       */
3071      public function set_module_viewed() {
3072          global $CFG;
3073          require_once($CFG->libdir . '/completionlib.php');
3074  
3075          // Trigger module viewed event.
3076          $event = \mod_lesson\event\course_module_viewed::create(array(
3077              'objectid' => $this->properties->id,
3078              'context' => $this->get_context()
3079          ));
3080          $event->add_record_snapshot('course_modules', $this->get_cm());
3081          $event->add_record_snapshot('course', $this->get_courserecord());
3082          $event->trigger();
3083  
3084          // Mark as viewed.
3085          $completion = new completion_info($this->get_courserecord());
3086          $completion->set_module_viewed($this->get_cm());
3087      }
3088  
3089      /**
3090       * Return the timers in the current lesson for the given user.
3091       *
3092       * @param  int      $userid    the user id
3093       * @param  string   $sort      an order to sort the results in (optional, a valid SQL ORDER BY parameter).
3094       * @param  string   $fields    a comma separated list of fields to return
3095       * @param  int      $limitfrom return a subset of records, starting at this point (optional).
3096       * @param  int      $limitnum  return a subset comprising this many records in total (optional, required if $limitfrom is set).
3097       * @return array    list of timers for the given user in the lesson
3098       * @since  Moodle 3.3
3099       */
3100      public function get_user_timers($userid = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) {
3101          global $DB, $USER;
3102  
3103          if ($userid === null) {
3104              $userid = $USER->id;
3105          }
3106  
3107          $params = array('lessonid' => $this->properties->id, 'userid' => $userid);
3108          return $DB->get_records('lesson_timer', $params, $sort, $fields, $limitfrom, $limitnum);
3109      }
3110  
3111      /**
3112       * Check if the user is out of time in a timed lesson.
3113       *
3114       * @param  stdClass $timer timer object
3115       * @return bool True if the user is on time, false is the user ran out of time
3116       * @since  Moodle 3.3
3117       */
3118      public function check_time($timer) {
3119          if ($this->properties->timelimit) {
3120              $timeleft = $timer->starttime + $this->properties->timelimit - time();
3121              if ($timeleft <= 0) {
3122                  // Out of time.
3123                  $this->add_message(get_string('eolstudentoutoftime', 'lesson'));
3124                  return false;
3125              } else if ($timeleft < 60) {
3126                  // One minute warning.
3127                  $this->add_message(get_string('studentoneminwarning', 'lesson'));
3128              }
3129          }
3130          return true;
3131      }
3132  
3133      /**
3134       * Add different informative messages to the given page.
3135       *
3136       * @param lesson_page $page page object
3137       * @param reviewmode $bool whether we are in review mode or not
3138       * @since  Moodle 3.3
3139       */
3140      public function add_messages_on_page_view(lesson_page $page, $reviewmode) {
3141          global $DB, $USER;
3142  
3143          if (!$this->can_manage()) {
3144              if ($page->qtype == LESSON_PAGE_BRANCHTABLE && $this->properties->minquestions) {
3145                  // Tell student how many questions they have seen, how many are required and their grade.
3146                  $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3147                  $gradeinfo = lesson_grade($this, $ntries);
3148                  if ($gradeinfo->attempts) {
3149                      if ($gradeinfo->nquestions < $this->properties->minquestions) {
3150                          $a = new stdClass;
3151                          $a->nquestions   = $gradeinfo->nquestions;
3152                          $a->minquestions = $this->properties->minquestions;
3153                          $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
3154                      }
3155  
3156                      if (!$reviewmode && $this->properties->ongoing) {
3157                          $this->add_message(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'notify');
3158                          if ($this->properties->grade != GRADE_TYPE_NONE) {
3159                              $a = new stdClass;
3160                              $a->grade = format_float($gradeinfo->grade * $this->properties->grade / 100, 1);
3161                              $a->total = $this->properties->grade;
3162                              $this->add_message(get_string('yourcurrentgradeisoutof', 'lesson', $a), 'notify');
3163                          }
3164                      }
3165                  }
3166              }
3167          } else {
3168              if ($this->properties->timelimit) {
3169                  $this->add_message(get_string('teachertimerwarning', 'lesson'));
3170              }
3171              if (lesson_display_teacher_warning($this)) {
3172                  // This is the warning msg for teachers to inform them that cluster
3173                  // and unseen does not work while logged in as a teacher.
3174                  $warningvars = new stdClass();
3175                  $warningvars->cluster = get_string('clusterjump', 'lesson');
3176                  $warningvars->unseen = get_string('unseenpageinbranch', 'lesson');
3177                  $this->add_message(get_string('teacherjumpwarning', 'lesson', $warningvars));
3178              }
3179          }
3180      }
3181  
3182      /**
3183       * Get the ongoing score message for the user (depending on the user permission and lesson settings).
3184       *
3185       * @return str the ongoing score message
3186       * @since  Moodle 3.3
3187       */
3188      public function get_ongoing_score_message() {
3189          global $USER, $DB;
3190  
3191          $context = $this->get_context();
3192  
3193          if (has_capability('mod/lesson:manage', $context)) {
3194              return get_string('teacherongoingwarning', 'lesson');
3195          } else {
3196              $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3197              if (isset($USER->modattempts[$this->properties->id])) {
3198                  $ntries--;
3199              }
3200              $gradeinfo = lesson_grade($this, $ntries);
3201              $a = new stdClass;
3202              if ($this->properties->custom) {
3203                  $a->score = $gradeinfo->earned;
3204                  $a->currenthigh = $gradeinfo->total;
3205                  return get_string("ongoingcustom", "lesson", $a);
3206              } else {
3207                  $a->correct = $gradeinfo->earned;
3208                  $a->viewed = $gradeinfo->attempts;
3209                  return get_string("ongoingnormal", "lesson", $a);
3210              }
3211          }
3212      }
3213  
3214      /**
3215       * Calculate the progress of the current user in the lesson.
3216       *
3217       * @return int the progress (scale 0-100)
3218       * @since  Moodle 3.3
3219       */
3220      public function calculate_progress() {
3221          global $USER, $DB;
3222  
3223          // Check if the user is reviewing the attempt.
3224          if (isset($USER->modattempts[$this->properties->id])) {
3225              return 100;
3226          }
3227  
3228          // All of the lesson pages.
3229          $pages = $this->load_all_pages();
3230          foreach ($pages as $page) {
3231              if ($page->prevpageid == 0) {
3232                  $pageid = $page->id;  // Find the first page id.
3233                  break;
3234              }
3235          }
3236  
3237          // Current attempt number.
3238          if (!$ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id))) {
3239              $ntries = 0;  // May not be necessary.
3240          }
3241  
3242          $viewedpageids = array();
3243          if ($attempts = $this->get_attempts($ntries, false)) {
3244              foreach ($attempts as $attempt) {
3245                  $viewedpageids[$attempt->pageid] = $attempt;
3246              }
3247          }
3248  
3249          $viewedbranches = array();
3250          // Collect all of the branch tables viewed.
3251          if ($branches = $this->get_content_pages_viewed($ntries, $USER->id, 'timeseen ASC', 'id, pageid')) {
3252              foreach ($branches as $branch) {
3253                  $viewedbranches[$branch->pageid] = $branch;
3254              }
3255              $viewedpageids = array_merge($viewedpageids, $viewedbranches);
3256          }
3257  
3258          // Filter out the following pages:
3259          // - End of Cluster
3260          // - End of Branch
3261          // - Pages found inside of Clusters
3262          // Do not filter out Cluster Page(s) because we count a cluster as one.
3263          // By keeping the cluster page, we get our 1.
3264          $validpages = array();
3265          while ($pageid != 0) {
3266              $pageid = $pages[$pageid]->valid_page_and_view($validpages, $viewedpageids);
3267          }
3268  
3269          // Progress calculation as a percent.
3270          $progress = round(count($viewedpageids) / count($validpages), 2) * 100;
3271          return (int) $progress;
3272      }
3273  
3274      /**
3275       * Calculate the correct page and prepare contents for a given page id (could be a page jump id).
3276       *
3277       * @param  int $pageid the given page id
3278       * @param  mod_lesson_renderer $lessonoutput the lesson output rendered
3279       * @param  bool $reviewmode whether we are in review mode or not
3280       * @param  bool $redirect  Optional, default to true. Set to false to avoid redirection and return the page to redirect.
3281       * @return array the page object and contents
3282       * @throws moodle_exception
3283       * @since  Moodle 3.3
3284       */
3285      public function prepare_page_and_contents($pageid, $lessonoutput, $reviewmode, $redirect = true) {
3286          global $USER, $CFG;
3287  
3288          $page = $this->load_page($pageid);
3289          // Check if the page is of a special type and if so take any nessecary action.
3290          $newpageid = $page->callback_on_view($this->can_manage(), $redirect);
3291  
3292          // Avoid redirections returning the jump to special page id.
3293          if (!$redirect && is_numeric($newpageid) && $newpageid < 0) {
3294              return array($newpageid, null, null);
3295          }
3296  
3297          if (is_numeric($newpageid)) {
3298              $page = $this->load_page($newpageid);
3299          }
3300  
3301          // Add different informative messages to the given page.
3302          $this->add_messages_on_page_view($page, $reviewmode);
3303  
3304          if (is_array($page->answers) && count($page->answers) > 0) {
3305              // This is for modattempts option.  Find the users previous answer to this page,
3306              // and then display it below in answer processing.
3307              if (isset($USER->modattempts[$this->properties->id])) {
3308                  $retries = $this->count_user_retries($USER->id);
3309                  if (!$attempts = $this->get_attempts($retries - 1, false, $page->id)) {
3310                      throw new moodle_exception('cannotfindpreattempt', 'lesson');
3311                  }
3312                  $attempt = end($attempts);
3313                  $USER->modattempts[$this->properties->id] = $attempt;
3314              } else {
3315                  $attempt = false;
3316              }
3317              $lessoncontent = $lessonoutput->display_page($this, $page, $attempt);
3318          } else {
3319              require_once($CFG->dirroot . '/mod/lesson/view_form.php');
3320              $data = new stdClass;
3321              $data->id = $this->get_cm()->id;
3322              $data->pageid = $page->id;
3323              $data->newpageid = $this->get_next_page($page->nextpageid);
3324  
3325              $customdata = array(
3326                  'title'     => $page->title,
3327                  'contents'  => $page->get_contents()
3328              );
3329              $mform = new lesson_page_without_answers($CFG->wwwroot.'/mod/lesson/continue.php', $customdata);
3330              $mform->set_data($data);
3331              ob_start();
3332              $mform->display();
3333              $lessoncontent = ob_get_contents();
3334              ob_end_clean();
3335          }
3336  
3337          return array($page->id, $page, $lessoncontent);
3338      }
3339  
3340      /**
3341       * This returns a real page id to jump to (or LESSON_EOL) after processing page responses.
3342       *
3343       * @param  lesson_page $page      lesson page
3344       * @param  int         $newpageid the new page id
3345       * @return int the real page to jump to (or end of lesson)
3346       * @since  Moodle 3.3
3347       */
3348      public function calculate_new_page_on_jump(lesson_page $page, $newpageid) {
3349          global $USER, $DB;
3350  
3351          $canmanage = $this->can_manage();
3352  
3353          if (isset($USER->modattempts[$this->properties->id])) {
3354              // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time.
3355              if ($USER->modattempts[$this->properties->id]->pageid == $page->id && $page->nextpageid == 0) {
3356                  // Remember, this session variable holds the pageid of the last page that the user saw.
3357                  $newpageid = LESSON_EOL;
3358              } else {
3359                  $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3360                  $nretakes--; // Make sure we are looking at the right try.
3361                  $attempts = $DB->get_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes), "timeseen", "id, pageid");
3362                  $found = false;
3363                  $temppageid = 0;
3364                  // Make sure that the newpageid always defaults to something valid.
3365                  $newpageid = LESSON_EOL;
3366                  foreach ($attempts as $attempt) {
3367                      if ($found && $temppageid != $attempt->pageid) {
3368                          // Now try to find the next page, make sure next few attempts do no belong to current page.
3369                          $newpageid = $attempt->pageid;
3370                          break;
3371                      }
3372                      if ($attempt->pageid == $page->id) {
3373                          $found = true; // If found current page.
3374                          $temppageid = $attempt->pageid;
3375                      }
3376                  }
3377              }
3378          } else if ($newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $newpageid > 0) {
3379              // Going to check to see if the page that the user is going to view next, is a cluster page.
3380              // If so, dont display, go into the cluster.
3381              // The $newpageid > 0 is used to filter out all of the negative code jumps.
3382              $newpage = $this->load_page($newpageid);
3383              if ($overridenewpageid = $newpage->override_next_page($newpageid)) {
3384                  $newpageid = $overridenewpageid;
3385              }
3386          } else if ($newpageid == LESSON_UNSEENBRANCHPAGE) {
3387              if ($canmanage) {
3388                  if ($page->nextpageid == 0) {
3389                      $newpageid = LESSON_EOL;
3390                  } else {
3391                      $newpageid = $page->nextpageid;
3392                  }
3393              } else {
3394                  $newpageid = lesson_unseen_question_jump($this, $USER->id, $page->id);
3395              }
3396          } else if ($newpageid == LESSON_PREVIOUSPAGE) {
3397              $newpageid = $page->prevpageid;
3398          } else if ($newpageid == LESSON_RANDOMPAGE) {
3399              $newpageid = lesson_random_question_jump($this, $page->id);
3400          } else if ($newpageid == LESSON_CLUSTERJUMP) {
3401              if ($canmanage) {
3402                  if ($page->nextpageid == 0) {  // If teacher, go to next page.
3403                      $newpageid = LESSON_EOL;
3404                  } else {
3405                      $newpageid = $page->nextpageid;
3406                  }
3407              } else {
3408                  $newpageid = $this->cluster_jump($page->id);
3409              }
3410          } else if ($newpageid == 0) {
3411              $newpageid = $page->id;
3412          } else if ($newpageid == LESSON_NEXTPAGE) {
3413              $newpageid = $this->get_next_page($page->nextpageid);
3414          }
3415  
3416          return $newpageid;
3417      }
3418  
3419      /**
3420       * Process page responses.
3421       *
3422       * @param lesson_page $page page object
3423       * @since  Moodle 3.3
3424       */
3425      public function process_page_responses(lesson_page $page) {
3426          $context = $this->get_context();
3427  
3428          // Check the page has answers [MDL-25632].
3429          if (count($page->answers) > 0) {
3430              $result = $page->record_attempt($context);
3431          } else {
3432              // The page has no answers so we will just progress to the next page in the
3433              // sequence (as set by newpageid).
3434              $result = new stdClass;
3435              $result->newpageid       = optional_param('newpageid', $page->nextpageid, PARAM_INT);
3436              $result->nodefaultresponse  = true;
3437              $result->inmediatejump = false;
3438          }
3439  
3440          if ($result->inmediatejump) {
3441              return $result;
3442          }
3443  
3444          $result->newpageid = $this->calculate_new_page_on_jump($page, $result->newpageid);
3445  
3446          return $result;
3447      }
3448  
3449      /**
3450       * Add different informative messages to the given page.
3451       *
3452       * @param lesson_page $page page object
3453       * @param stdClass $result the page processing result object
3454       * @param bool $reviewmode whether we are in review mode or not
3455       * @since  Moodle 3.3
3456       */
3457      public function add_messages_on_page_process(lesson_page $page, $result, $reviewmode) {
3458  
3459          if ($this->can_manage()) {
3460              // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher.
3461              if (lesson_display_teacher_warning($this)) {
3462                  $warningvars = new stdClass();
3463                  $warningvars->cluster = get_string("clusterjump", "lesson");
3464                  $warningvars->unseen = get_string("unseenpageinbranch", "lesson");
3465                  $this->add_message(get_string("teacherjumpwarning", "lesson", $warningvars));
3466              }
3467              // Inform teacher that s/he will not see the timer.
3468              if ($this->properties->timelimit) {
3469                  $this->add_message(get_string("teachertimerwarning", "lesson"));
3470              }
3471          }
3472          // Report attempts remaining.
3473          if ($result->attemptsremaining != 0 && $this->properties->review && !$reviewmode) {
3474              $this->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining));
3475          }
3476      }
3477  
3478      /**
3479       * Process and return all the information for the end of lesson page.
3480       *
3481       * @param string $outoftime used to check to see if the student ran out of time
3482       * @return stdclass an object with all the page data ready for rendering
3483       * @since  Moodle 3.3
3484       */
3485      public function process_eol_page($outoftime) {
3486          global $DB, $USER;
3487  
3488          $course = $this->get_courserecord();
3489          $cm = $this->get_cm();
3490          $canmanage = $this->can_manage();
3491  
3492          // Init all the possible fields and values.
3493          $data = (object) array(
3494              'gradelesson' => true,
3495              'notenoughtimespent' => false,
3496              'numberofpagesviewed' => false,
3497              'youshouldview' => false,
3498              'numberofcorrectanswers' => false,
3499              'displayscorewithessays' => false,
3500              'displayscorewithoutessays' => false,
3501              'yourcurrentgradeisoutof' => false,
3502              'yourcurrentgradeis' => false,
3503              'eolstudentoutoftimenoanswers' => false,
3504              'welldone' => false,
3505              'progressbar' => false,
3506              'displayofgrade' => false,
3507              'reviewlesson' => false,
3508              'modattemptsnoteacher' => false,
3509              'activitylink' => false,
3510              'progresscompleted' => false,
3511          );
3512  
3513          $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3514          if (isset($USER->modattempts[$this->properties->id])) {
3515              $ntries--;  // Need to look at the old attempts :).
3516          }
3517  
3518          $gradeinfo = lesson_grade($this, $ntries);
3519          $data->gradeinfo = $gradeinfo;
3520          if ($this->properties->custom && !$canmanage) {
3521              // Before we calculate the custom score make sure they answered the minimum
3522              // number of questions. We only need to do this for custom scoring as we can
3523              // not get the miniumum score the user should achieve. If we are not using
3524              // custom scoring (so all questions are valued as 1) then we simply check if
3525              // they answered more than the minimum questions, if not, we mark it out of the
3526              // number specified in the minimum questions setting - which is done in lesson_grade().
3527              // Get the number of answers given.
3528              if ($gradeinfo->nquestions < $this->properties->minquestions) {
3529                  $data->gradelesson = false;
3530                  $a = new stdClass;
3531                  $a->nquestions = $gradeinfo->nquestions;
3532                  $a->minquestions = $this->properties->minquestions;
3533                  $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
3534              }
3535          }
3536  
3537          if (!$canmanage) {
3538              if ($data->gradelesson) {
3539                  // Store this now before any modifications to pages viewed.
3540                  $progresscompleted = $this->calculate_progress();
3541  
3542                  // Update the clock / get time information for this user.
3543                  $this->stop_timer();
3544  
3545                  // Update completion state.
3546                  $completion = new completion_info($course);
3547                  if ($completion->is_enabled($cm) && $this->properties->completionendreached) {
3548                      $completion->update_state($cm, COMPLETION_COMPLETE);
3549                  }
3550  
3551                  if ($this->properties->completiontimespent > 0) {
3552                      $duration = $DB->get_field_sql(
3553                          "SELECT SUM(lessontime - starttime)
3554                                         FROM {lesson_timer}
3555                                        WHERE lessonid = :lessonid
3556                                          AND userid = :userid",
3557                          array('userid' => $USER->id, 'lessonid' => $this->properties->id));
3558                      if (!$duration) {
3559                          $duration = 0;
3560                      }
3561  
3562                      // If student has not spend enough time in the lesson, display a message.
3563                      if ($duration < $this->properties->completiontimespent) {
3564                          $a = new stdClass;
3565                          $a->timespentraw = $duration;
3566                          $a->timespent = format_time($duration);
3567                          $a->timerequiredraw = $this->properties->completiontimespent;
3568                          $a->timerequired = format_time($this->properties->completiontimespent);
3569                          $data->notenoughtimespent = $a;
3570                      }
3571                  }
3572  
3573                  if ($gradeinfo->attempts) {
3574                      if (!$this->properties->custom) {
3575                          $data->numberofpagesviewed = $gradeinfo->nquestions;
3576                          if ($this->properties->minquestions) {
3577                              if ($gradeinfo->nquestions < $this->properties->minquestions) {
3578                                  $data->youshouldview = $this->properties->minquestions;
3579                              }
3580                          }
3581                          $data->numberofcorrectanswers = $gradeinfo->earned;
3582                      }
3583                      $a = new stdClass;
3584                      $a->score = $gradeinfo->earned;
3585                      $a->grade = $gradeinfo->total;
3586                      if ($gradeinfo->nmanual) {
3587                          $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints;
3588                          $a->essayquestions = $gradeinfo->nmanual;
3589                          $data->displayscorewithessays = $a;
3590                      } else {
3591                          $data->displayscorewithoutessays = $a;
3592                      }
3593  
3594                      $grade = new stdClass();
3595                      $grade->lessonid = $this->properties->id;
3596                      $grade->userid = $USER->id;
3597                      $grade->grade = $gradeinfo->grade;
3598                      $grade->completed = time();
3599                      if (isset($USER->modattempts[$this->properties->id])) { // If reviewing, make sure update old grade record.
3600                          if (!$grades = $DB->get_records("lesson_grades",
3601                              array("lessonid" => $this->properties->id, "userid" => $USER->id), "completed DESC", '*', 0, 1)) {
3602                              throw new moodle_exception('cannotfindgrade', 'lesson');
3603                          }
3604                          $oldgrade = array_shift($grades);
3605                          $grade->id = $oldgrade->id;
3606                          $DB->update_record("lesson_grades", $grade);
3607                      } else {
3608                          $newgradeid = $DB->insert_record("lesson_grades", $grade);
3609                      }
3610  
3611                      // Update central gradebook.
3612                      lesson_update_grades($this, $USER->id);
3613  
3614                      // Print grade (grade type Point).
3615                      if ($this->properties->grade > 0) {
3616                          $a = new stdClass;
3617                          $a->grade = format_float($gradeinfo->grade * $this->properties->grade / 100, 1);
3618                          $a->total = $this->properties->grade;
3619                          $data->yourcurrentgradeisoutof = $a;
3620                      }
3621  
3622                      // Print grade (grade type Scale).
3623                      if ($this->properties->grade < 0) {
3624                          // Grade type is Scale.
3625                          $grades = grade_get_grades($course->id, 'mod', 'lesson', $cm->instance, $USER->id);
3626                          $grade = reset($grades->items[0]->grades);
3627                          $data->yourcurrentgradeis = $grade->str_grade;
3628                      }
3629                  } else {
3630                      if ($this->properties->timelimit) {
3631                          if ($outoftime == 'normal') {
3632                              $grade = new stdClass();
3633                              $grade->lessonid = $this->properties->id;
3634                              $grade->userid = $USER->id;
3635                              $grade->grade = 0;
3636                              $grade->completed = time();
3637                              $newgradeid = $DB->insert_record("lesson_grades", $grade);
3638                              $data->eolstudentoutoftimenoanswers = true;
3639  
3640                              // Update central gradebook.
3641                              lesson_update_grades($this, $USER->id);
3642                          }
3643                      } else {
3644                          $data->welldone = true;
3645                      }
3646                  }
3647  
3648                  $data->progresscompleted = $progresscompleted;
3649              }
3650          } else {
3651              // Display for teacher.
3652              if ($this->properties->grade != GRADE_TYPE_NONE) {
3653                  $data->displayofgrade = true;
3654              }
3655          }
3656  
3657          if ($this->properties->modattempts && !$canmanage) {
3658              // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time
3659              // look at the attempt records to find the first QUESTION page that the user answered, then use that page id
3660              // to pass to view again.  This is slick cause it wont call the empty($pageid) code
3661              // $ntries is decremented above.
3662              if (!$attempts = $this->get_attempts($ntries)) {
3663                  $attempts = array();
3664                  $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id));
3665              } else {
3666                  $firstattempt = current($attempts);
3667                  $pageid = $firstattempt->pageid;
3668                  // If the student wishes to review, need to know the last question page that the student answered.
3669                  // This will help to make sure that the student can leave the lesson via pushing the continue button.
3670                  $lastattempt = end($attempts);
3671                  $USER->modattempts[$this->properties->id] = $lastattempt->pageid;
3672  
3673                  $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id, 'pageid' => $pageid));
3674              }
3675              $data->reviewlesson = $url->out(false);
3676          } else if ($this->properties->modattempts && $canmanage) {
3677              $data->modattemptsnoteacher = true;
3678          }
3679  
3680          if ($this->properties->activitylink) {
3681              $data->activitylink = $this->link_for_activitylink();
3682          }
3683          return $data;
3684      }
3685  
3686      /**
3687       * Returns the last "legal" attempt from the list of student attempts.
3688       *
3689       * @param array $attempts The list of student attempts.
3690       * @return stdClass The updated fom data.
3691       */
3692      public function get_last_attempt(array $attempts): stdClass {
3693          // If there are more tries than the max that is allowed, grab the last "legal" attempt.
3694          if (!empty($this->maxattempts) && (count($attempts) > $this->maxattempts)) {
3695              $lastattempt = $attempts[$this->maxattempts - 1];
3696          } else {
3697              // Grab the last attempt since there's no limit to the max attempts or the user has made fewer attempts than the max.
3698              $lastattempt = end($attempts);
3699          }
3700          return $lastattempt;
3701      }
3702  }
3703  
3704  
3705  /**
3706   * Abstract class to provide a core functions to the all lesson classes
3707   *
3708   * This class should be abstracted by ALL classes with the lesson module to ensure
3709   * that all classes within this module can be interacted with in the same way.
3710   *
3711   * This class provides the user with a basic properties array that can be fetched
3712   * or set via magic methods, or alternatively by defining methods get_blah() or
3713   * set_blah() within the extending object.
3714   *
3715   * @copyright  2009 Sam Hemelryk
3716   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3717   */
3718  abstract class lesson_base {
3719  
3720      /**
3721       * An object containing properties
3722       * @var stdClass
3723       */
3724      protected $properties;
3725  
3726      /**
3727       * The constructor
3728       * @param stdClass $properties
3729       */
3730      public function __construct($properties) {
3731          $this->properties = (object)$properties;
3732      }
3733  
3734      /**
3735       * Magic property method
3736       *
3737       * Attempts to call a set_$key method if one exists otherwise falls back
3738       * to simply set the property
3739       *
3740       * @param string $key
3741       * @param mixed $value
3742       */
3743      public function __set($key, $value) {
3744          if (method_exists($this, 'set_'.$key)) {
3745              $this->{'set_'.$key}($value);
3746          }
3747          $this->properties->{$key} = $value;
3748      }
3749  
3750      /**
3751       * Magic get method
3752       *
3753       * Attempts to call a get_$key method to return the property and ralls over
3754       * to return the raw property
3755       *
3756       * @param str $key
3757       * @return mixed
3758       */
3759      public function __get($key) {
3760          if (method_exists($this, 'get_'.$key)) {
3761              return $this->{'get_'.$key}();
3762          }
3763          return $this->properties->{$key};
3764      }
3765  
3766      /**
3767       * Stupid PHP needs an isset magic method if you use the get magic method and
3768       * still want empty calls to work.... blah ~!
3769       *
3770       * @param string $key
3771       * @return bool
3772       */
3773      public function __isset($key) {
3774          if (method_exists($this, 'get_'.$key)) {
3775              $val = $this->{'get_'.$key}();
3776              return !empty($val);
3777          }
3778          return !empty($this->properties->{$key});
3779      }
3780  
3781      //NOTE: E_STRICT does not allow to change function signature!
3782  
3783      /**
3784       * If implemented should create a new instance, save it in the DB and return it
3785       */
3786      //public static function create() {}
3787      /**
3788       * If implemented should load an instance from the DB and return it
3789       */
3790      //public static function load() {}
3791      /**
3792       * Fetches all of the properties of the object
3793       * @return stdClass
3794       */
3795      public function properties() {
3796          return $this->properties;
3797      }
3798  }
3799  
3800  
3801  /**
3802   * Abstract class representation of a page associated with a lesson.
3803   *
3804   * This class should MUST be extended by all specialised page types defined in
3805   * mod/lesson/pagetypes/.
3806   * There are a handful of abstract methods that need to be defined as well as
3807   * severl methods that can optionally be defined in order to make the page type
3808   * operate in the desired way
3809   *
3810   * Database properties
3811   * @property int $id The id of this lesson page
3812   * @property int $lessonid The id of the lesson this page belongs to
3813   * @property int $prevpageid The id of the page before this one
3814   * @property int $nextpageid The id of the next page in the page sequence
3815   * @property int $qtype Identifies the page type of this page
3816   * @property int $qoption Used to record page type specific options
3817   * @property int $layout Used to record page specific layout selections
3818   * @property int $display Used to record page specific display selections
3819   * @property int $timecreated Timestamp for when the page was created
3820   * @property int $timemodified Timestamp for when the page was last modified
3821   * @property string $title The title of this page
3822   * @property string $contents The rich content shown to describe the page
3823   * @property int $contentsformat The format of the contents field
3824   *
3825   * Calculated properties
3826   * @property-read array $answers An array of answers for this page
3827   * @property-read bool $displayinmenublock Toggles display in the left menu block
3828   * @property-read array $jumps An array containing all the jumps this page uses
3829   * @property-read lesson $lesson The lesson this page belongs to
3830   * @property-read int $type The type of the page [question | structure]
3831   * @property-read typeid The unique identifier for the page type
3832   * @property-read typestring The string that describes this page type
3833   *
3834   * @abstract
3835   * @copyright  2009 Sam Hemelryk
3836   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3837   */
3838  abstract class lesson_page extends lesson_base {
3839  
3840      /**
3841       * A reference to the lesson this page belongs to
3842       * @var lesson
3843       */
3844      protected $lesson = null;
3845      /**
3846       * Contains the answers to this lesson_page once loaded
3847       * @var null|array
3848       */
3849      protected $answers = null;
3850      /**
3851       * This sets the type of the page, can be one of the constants defined below
3852       * @var int
3853       */
3854      protected $type = 0;
3855  
3856      /**
3857       * Constants used to identify the type of the page
3858       */
3859      const TYPE_QUESTION = 0;
3860      const TYPE_STRUCTURE = 1;
3861  
3862      /**
3863       * Constant used as a delimiter when parsing multianswer questions
3864       */
3865      const MULTIANSWER_DELIMITER = '@^#|';
3866  
3867      /**
3868       * This method should return the integer used to identify the page type within
3869       * the database and throughout code. This maps back to the defines used in 1.x
3870       * @abstract
3871       * @return int
3872       */
3873      abstract protected function get_typeid();
3874      /**
3875       * This method should return the string that describes the pagetype
3876       * @abstract
3877       * @return string
3878       */
3879      abstract protected function get_typestring();
3880  
3881      /**
3882       * This method gets called to display the page to the user taking the lesson
3883       * @abstract
3884       * @param object $renderer
3885       * @param object $attempt
3886       * @return string
3887       */
3888      abstract public function display($renderer, $attempt);
3889  
3890      /**
3891       * Creates a new lesson_page within the database and returns the correct pagetype
3892       * object to use to interact with the new lesson
3893       *
3894       * @final
3895       * @static
3896       * @param object $properties
3897       * @param lesson $lesson
3898       * @return lesson_page Specialised object that extends lesson_page
3899       */
3900      final public static function create($properties, lesson $lesson, $context, $maxbytes) {
3901          global $DB;
3902          $newpage = new stdClass;
3903          $newpage->title = $properties->title;
3904          $newpage->contents = $properties->contents_editor['text'];
3905          $newpage->contentsformat = $properties->contents_editor['format'];
3906          $newpage->lessonid = $lesson->id;
3907          $newpage->timecreated = time();
3908          $newpage->qtype = $properties->qtype;
3909          $newpage->qoption = (isset($properties->qoption))?1:0;
3910          $newpage->layout = (isset($properties->layout))?1:0;
3911          $newpage->display = (isset($properties->display))?1:0;
3912          $newpage->prevpageid = 0; // this is a first page
3913          $newpage->nextpageid = 0; // this is the only page
3914  
3915          if ($properties->pageid) {
3916              $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid');
3917              if (!$prevpage) {
3918                  print_error('cannotfindpages', 'lesson');
3919              }
3920              $newpage->prevpageid = $prevpage->id;
3921              $newpage->nextpageid = $prevpage->nextpageid;
3922          } else {
3923              $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id');
3924              if ($nextpage) {
3925                  // This is the first page, there are existing pages put this at the start
3926                  $newpage->nextpageid = $nextpage->id;
3927              }
3928          }
3929  
3930          $newpage->id = $DB->insert_record("lesson_pages", $newpage);
3931  
3932          $editor = new stdClass;
3933          $editor->id = $newpage->id;
3934          $editor->contents_editor = $properties->contents_editor;
3935          $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $editor->id);
3936          $DB->update_record("lesson_pages", $editor);
3937  
3938          if ($newpage->prevpageid > 0) {
3939              $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid));
3940          }
3941          if ($newpage->nextpageid > 0) {
3942              $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid));
3943          }
3944  
3945          $page = lesson_page::load($newpage, $lesson);
3946          $page->create_answers($properties);
3947  
3948          // Trigger an event: page created.
3949          $eventparams = array(
3950              'context' => $context,
3951              'objectid' => $newpage->id,
3952              'other' => array(
3953                  'pagetype' => $page->get_typestring()
3954                  )
3955              );
3956          $event = \mod_lesson\event\page_created::create($eventparams);
3957          $snapshot = clone($newpage);
3958          $snapshot->timemodified = 0;
3959          $event->add_record_snapshot('lesson_pages', $snapshot);
3960          $event->trigger();
3961  
3962          $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess');
3963  
3964          return $page;
3965      }
3966  
3967      /**
3968       * This method loads a page object from the database and returns it as a
3969       * specialised object that extends lesson_page
3970       *
3971       * @final
3972       * @static
3973       * @param int $id
3974       * @param lesson $lesson
3975       * @return lesson_page Specialised lesson_page object
3976       */
3977      final public static function load($id, lesson $lesson) {
3978          global $DB;
3979  
3980          if (is_object($id) && !empty($id->qtype)) {
3981              $page = $id;
3982          } else {
3983              $page = $DB->get_record("lesson_pages", array("id" => $id));
3984              if (!$page) {
3985                  print_error('cannotfindpages', 'lesson');
3986              }
3987          }
3988          $manager = lesson_page_type_manager::get($lesson);
3989  
3990          $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype);
3991          if (!class_exists($class)) {
3992              $class = 'lesson_page';
3993          }
3994  
3995          return new $class($page, $lesson);
3996      }
3997  
3998      /**
3999       * Deletes a lesson_page from the database as well as any associated records.
4000       * @final
4001       * @return bool
4002       */
4003      final public function delete() {
4004          global $DB;
4005  
4006          $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
4007          $context = context_module::instance($cm->id);
4008  
4009          // Delete files associated with attempts.
4010          $fs = get_file_storage();
4011          if ($attempts = $DB->get_records('lesson_attempts', array("pageid" => $this->properties->id))) {
4012              foreach ($attempts as $attempt) {
4013                  $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $attempt->id);
4014                  $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers', $attempt->id);
4015              }
4016          }
4017  
4018          // Then delete all the associated records...
4019          $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id));
4020  
4021          $DB->delete_records("lesson_branch", array("pageid" => $this->properties->id));
4022  
4023          // Delete files related to answers and responses.
4024          if ($answers = $DB->get_records("lesson_answers", array("pageid" => $this->properties->id))) {
4025              foreach ($answers as $answer) {
4026                  $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $answer->id);
4027                  $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $answer->id);
4028              }
4029          }
4030  
4031          // ...now delete the answers...
4032          $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id));
4033          // ..and the page itself
4034          $DB->delete_records("lesson_pages", array("id" => $this->properties->id));
4035  
4036          // Trigger an event: page deleted.
4037          $eventparams = array(
4038              'context' => $context,
4039              'objectid' => $this->properties->id,
4040              'other' => array(
4041                  'pagetype' => $this->get_typestring()
4042                  )
4043              );
4044          $event = \mod_lesson\event\page_deleted::create($eventparams);
4045          $event->add_record_snapshot('lesson_pages', $this->properties);
4046          $event->trigger();
4047  
4048          // Delete files associated with this page.
4049          $fs->delete_area_files($context->id, 'mod_lesson', 'page_contents', $this->properties->id);
4050  
4051          // repair the hole in the linkage
4052          if (!$this->properties->prevpageid && !$this->properties->nextpageid) {
4053              //This is the only page, no repair needed
4054          } elseif (!$this->properties->prevpageid) {
4055              // this is the first page...
4056              $page = $this->lesson->load_page($this->properties->nextpageid);
4057              $page->move(null, 0);
4058          } elseif (!$this->properties->nextpageid) {
4059              // this is the last page...
4060              $page = $this->lesson->load_page($this->properties->prevpageid);
4061              $page->move(0);
4062          } else {
4063              // page is in the middle...
4064              $prevpage = $this->lesson->load_page($this->properties->prevpageid);
4065              $nextpage = $this->lesson->load_page($this->properties->nextpageid);
4066  
4067              $prevpage->move($nextpage->id);
4068              $nextpage->move(null, $prevpage->id);
4069          }
4070          return true;
4071      }
4072  
4073      /**
4074       * Moves a page by updating its nextpageid and prevpageid values within
4075       * the database
4076       *
4077       * @final
4078       * @param int $nextpageid
4079       * @param int $prevpageid
4080       */
4081      final public function move($nextpageid=null, $prevpageid=null) {
4082          global $DB;
4083          if ($nextpageid === null) {
4084              $nextpageid = $this->properties->nextpageid;
4085          }
4086          if ($prevpageid === null) {
4087              $prevpageid = $this->properties->prevpageid;
4088          }
4089          $obj = new stdClass;
4090          $obj->id = $this->properties->id;
4091          $obj->prevpageid = $prevpageid;
4092          $obj->nextpageid = $nextpageid;
4093          $DB->update_record('lesson_pages', $obj);
4094      }
4095  
4096      /**
4097       * Returns the answers that are associated with this page in the database
4098       *
4099       * @final
4100       * @return array
4101       */
4102      final public function get_answers() {
4103          global $DB;
4104          if ($this->answers === null) {
4105              $this->answers = array();
4106              $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id');
4107              if (!$answers) {
4108                  // It is possible that a lesson upgraded from Moodle 1.9 still
4109                  // contains questions without any answers [MDL-25632].
4110                  // debugging(get_string('cannotfindanswer', 'lesson'));
4111                  return array();
4112              }
4113              foreach ($answers as $answer) {
4114                  $this->answers[count($this->answers)] = new lesson_page_answer($answer);
4115              }
4116          }
4117          return $this->answers;
4118      }
4119  
4120      /**
4121       * Returns the lesson this page is associated with
4122       * @final
4123       * @return lesson
4124       */
4125      final protected function get_lesson() {
4126          return $this->lesson;
4127      }
4128  
4129      /**
4130       * Returns the type of page this is. Not to be confused with page type
4131       * @final
4132       * @return int
4133       */
4134      final protected function get_type() {
4135          return $this->type;
4136      }
4137  
4138      /**
4139       * Records an attempt at this page
4140       *
4141       * @final
4142       * @global moodle_database $DB
4143       * @param stdClass $context
4144       * @return stdClass Returns the result of the attempt
4145       */
4146      final public function record_attempt($context) {
4147          global $DB, $USER, $OUTPUT, $PAGE;
4148  
4149          /**
4150           * This should be overridden by each page type to actually check the response
4151           * against what ever custom criteria they have defined
4152           */
4153          $result = $this->check_answer();
4154  
4155          // Processes inmediate jumps.
4156          if ($result->inmediatejump) {
4157              return $result;
4158          }
4159  
4160          $result->attemptsremaining  = 0;
4161          $result->maxattemptsreached = false;
4162  
4163          if ($result->noanswer) {
4164              $result->newpageid = $this->properties->id; // display same page again
4165              $result->feedback  = get_string('noanswer', 'lesson');
4166          } else {
4167              if (!has_capability('mod/lesson:manage', $context)) {
4168                  $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
4169  
4170                  // Get the number of attempts that have been made on this question for this student and retake,
4171                  $nattempts = $DB->count_records('lesson_attempts', array('lessonid' => $this->lesson->id,
4172                      'userid' => $USER->id, 'pageid' => $this->properties->id, 'retry' => $nretakes));
4173  
4174                  // Check if they have reached (or exceeded) the maximum number of attempts allowed.
4175                  if (!empty($this->lesson->maxattempts) && $nattempts >= $this->lesson->maxattempts) {
4176                      $result->maxattemptsreached = true;
4177                      $result->feedback = get_string('maximumnumberofattemptsreached', 'lesson');
4178                      $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid);
4179                      return $result;
4180                  }
4181  
4182                  // record student's attempt
4183                  $attempt = new stdClass;
4184                  $attempt->lessonid = $this->lesson->id;
4185                  $attempt->pageid = $this->properties->id;
4186                  $attempt->userid = $USER->id;
4187                  $attempt->answerid = $result->answerid;
4188                  $attempt->retry = $nretakes;
4189                  $attempt->correct = $result->correctanswer;
4190                  if($result->userresponse !== null) {
4191                      $attempt->useranswer = $result->userresponse;
4192                  }
4193  
4194                  $attempt->timeseen = time();
4195                  // if allow modattempts, then update the old attempt record, otherwise, insert new answer record
4196                  $userisreviewing = false;
4197                  if (isset($USER->modattempts[$this->lesson->id])) {
4198                      $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high
4199                      $userisreviewing = true;
4200                  }
4201  
4202                  // Only insert a record if we are not reviewing the lesson.
4203                  if (!$userisreviewing) {
4204                      if ($this->lesson->retake || (!$this->lesson->retake && $nretakes == 0)) {
4205                          $attempt->id = $DB->insert_record("lesson_attempts", $attempt);
4206  
4207                          list($updatedattempt, $updatedresult) = $this->on_after_write_attempt($attempt, $result);
4208                          if ($updatedattempt) {
4209                              $attempt = $updatedattempt;
4210                              $result = $updatedresult;
4211                              $DB->update_record("lesson_attempts", $attempt);
4212                          }
4213  
4214                          // Trigger an event: question answered.
4215                          $eventparams = array(
4216                              'context' => context_module::instance($PAGE->cm->id),
4217                              'objectid' => $this->properties->id,
4218                              'other' => array(
4219                                  'pagetype' => $this->get_typestring()
4220                                  )
4221                              );
4222                          $event = \mod_lesson\event\question_answered::create($eventparams);
4223                          $event->add_record_snapshot('lesson_attempts', $attempt);
4224                          $event->trigger();
4225  
4226                          // Increase the number of attempts made.
4227                          $nattempts++;
4228                      }
4229                  } else {
4230                      // When reviewing the lesson, the existing attemptid is also needed for the filearea options.
4231                      $params = [
4232                          'lessonid' => $attempt->lessonid,
4233                          'pageid' => $attempt->pageid,
4234                          'userid' => $attempt->userid,
4235                          'answerid' => $attempt->answerid,
4236                          'retry' => $attempt->retry
4237                      ];
4238                      $attempt->id = $DB->get_field('lesson_attempts', 'id', $params);
4239                  }
4240                  // "number of attempts remaining" message if $this->lesson->maxattempts > 1
4241                  // displaying of message(s) is at the end of page for more ergonomic display
4242                  // If we are showing the number of remaining attempts, we need to show it regardless of what the next
4243                  // jump to page is.
4244                  if (!$result->correctanswer) {
4245                      // Retrieve the number of attempts left counter for displaying at bottom of feedback page.
4246                      if ($result->newpageid == 0 && !empty($this->lesson->maxattempts) && $nattempts >= $this->lesson->maxattempts) {
4247                          if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
4248                              $result->maxattemptsreached = true;
4249                          }
4250                          $result->newpageid = LESSON_NEXTPAGE;
4251                      } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
4252                          $result->attemptsremaining = $this->lesson->maxattempts - $nattempts;
4253                          if ($result->attemptsremaining == 0) {
4254                              $result->maxattemptsreached = true;
4255                          }
4256                      }
4257                  }
4258              }
4259  
4260              // Determine default feedback if necessary
4261              if (empty($result->response)) {
4262                  if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) {
4263                      // These conditions have been met:
4264                      //  1. The lesson manager has not supplied feedback to the student
4265                      //  2. Not displaying default feedback
4266                      //  3. The user did provide an answer
4267                      //  4. We are not reviewing with an incorrect answer (and not reviewing an essay question)
4268  
4269                      $result->nodefaultresponse = true;  // This will cause a redirect below
4270                  } else if ($result->isessayquestion) {
4271                      $result->response = get_string('defaultessayresponse', 'lesson');
4272                  } else if ($result->correctanswer) {
4273                      $result->response = get_string('thatsthecorrectanswer', 'lesson');
4274                  } else {
4275                      $result->response = get_string('thatsthewronganswer', 'lesson');
4276                  }
4277              }
4278  
4279              if ($result->response) {
4280                  if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) {
4281                      $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
4282                      $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id));
4283                      if ($qattempts == 1) {
4284                          $result->feedback = $OUTPUT->box(get_string("firstwrong", "lesson"), 'feedback');
4285                      } else {
4286                          if (!$result->maxattemptsreached) {
4287                              $result->feedback = $OUTPUT->box(get_string("secondpluswrong", "lesson"), 'feedback');
4288                          } else {
4289                              $result->feedback = $OUTPUT->box(get_string("finalwrong", "lesson"), 'feedback');
4290                          }
4291                      }
4292                  } else {
4293                      $result->feedback = '';
4294                  }
4295                  $class = 'response';
4296                  if ($result->correctanswer) {
4297                      $class .= ' correct'; // CSS over-ride this if they exist (!important).
4298                  } else if (!$result->isessayquestion) {
4299                      $class .= ' incorrect'; // CSS over-ride this if they exist (!important).
4300                  }
4301                  $options = new stdClass;
4302                  $options->noclean = true;
4303                  $options->para = true;
4304                  $options->overflowdiv = true;
4305                  $options->context = $context;
4306                  $options->attemptid = isset($attempt) ? $attempt->id : null;
4307  
4308                  $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options),
4309                          'generalbox boxaligncenter py-3');
4310                  $result->feedback .= '<div class="correctanswer generalbox"><em>'
4311                          . get_string("youranswer", "lesson").'</em> : <div class="studentanswer mt-2 mb-2">';
4312  
4313                  // Create a table containing the answers and responses.
4314                  $table = new html_table();
4315                  // Multianswer allowed.
4316                  if ($this->properties->qoption) {
4317                      $studentanswerarray = explode(self::MULTIANSWER_DELIMITER, $result->studentanswer);
4318                      $responsearr = explode(self::MULTIANSWER_DELIMITER, $result->response);
4319                      $studentanswerresponse = array_combine($studentanswerarray, $responsearr);
4320  
4321                      foreach ($studentanswerresponse as $answer => $response) {
4322                          // Add a table row containing the answer.
4323                          $studentanswer = $this->format_answer($answer, $context, $result->studentanswerformat, $options);
4324                          $table->data[] = array($studentanswer);
4325                          // If the response exists, add a table row containing the response. If not, add en empty row.
4326                          if (!empty(trim($response))) {
4327                              $studentresponse = isset($result->responseformat) ?
4328                                  $this->format_response($response, $context, $result->responseformat, $options) : $response;
4329                              $studentresponsecontent = html_writer::div('<em>' . get_string("response", "lesson") .
4330                                  '</em>: <br/>' . $studentresponse, $class);
4331                              $table->data[] = array($studentresponsecontent);
4332                          } else {
4333                              $table->data[] = array('');
4334                          }
4335                      }
4336                  } else {
4337                      // Add a table row containing the answer.
4338                      $studentanswer = $this->format_answer($result->studentanswer, $context, $result->studentanswerformat, $options);
4339                      $table->data[] = array($studentanswer);
4340                      // If the response exists, add a table row containing the response. If not, add en empty row.
4341                      if (!empty(trim($result->response))) {
4342                          $studentresponse = isset($result->responseformat) ?
4343                              $this->format_response($result->response, $context, $result->responseformat,
4344                                  $result->answerid, $options) : $result->response;
4345                          $studentresponsecontent = html_writer::div('<em>' . get_string("response", "lesson") .
4346                              '</em>: <br/>' . $studentresponse, $class);
4347                          $table->data[] = array($studentresponsecontent);
4348                      } else {
4349                          $table->data[] = array('');
4350                      }
4351                  }
4352  
4353                  $result->feedback .= html_writer::table($table).'</div></div>';
4354              }
4355          }
4356          return $result;
4357      }
4358  
4359      /**
4360       * Formats the answer. Override for custom formatting.
4361       *
4362       * @param string $answer
4363       * @param context $context
4364       * @param int $answerformat
4365       * @return string Returns formatted string
4366       */
4367      public function format_answer($answer, $context, $answerformat, $options = []) {
4368  
4369          if (is_object($options)) {
4370              $options = (array) $options;
4371          }
4372  
4373          if (empty($options['context'])) {
4374              $options['context'] = $context;
4375          }
4376  
4377          if (empty($options['para'])) {
4378              $options['para'] = true;
4379          }
4380  
4381          return format_text($answer, $answerformat, $options);
4382      }
4383  
4384      /**
4385       * Formats the response
4386       *
4387       * @param string $response
4388       * @param context $context
4389       * @param int $responseformat
4390       * @param int $answerid
4391       * @param stdClass $options
4392       * @return string Returns formatted string
4393       */
4394      private function format_response($response, $context, $responseformat, $answerid, $options) {
4395  
4396          $convertstudentresponse = file_rewrite_pluginfile_urls($response, 'pluginfile.php',
4397              $context->id, 'mod_lesson', 'page_responses', $answerid);
4398  
4399          return format_text($convertstudentresponse, $responseformat, $options);
4400      }
4401  
4402      /**
4403       * Returns the string for a jump name
4404       *
4405       * @final
4406       * @param int $jumpto Jump code or page ID
4407       * @return string
4408       **/
4409      final protected function get_jump_name($jumpto) {
4410          global $DB;
4411          static $jumpnames = array();
4412  
4413          if (!array_key_exists($jumpto, $jumpnames)) {
4414              if ($jumpto == LESSON_THISPAGE) {
4415                  $jumptitle = get_string('thispage', 'lesson');
4416              } elseif ($jumpto == LESSON_NEXTPAGE) {
4417                  $jumptitle = get_string('nextpage', 'lesson');
4418              } elseif ($jumpto == LESSON_EOL) {
4419                  $jumptitle = get_string('endoflesson', 'lesson');
4420              } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
4421                  $jumptitle = get_string('unseenpageinbranch', 'lesson');
4422              } elseif ($jumpto == LESSON_PREVIOUSPAGE) {
4423                  $jumptitle = get_string('previouspage', 'lesson');
4424              } elseif ($jumpto == LESSON_RANDOMPAGE) {
4425                  $jumptitle = get_string('randompageinbranch', 'lesson');
4426              } elseif ($jumpto == LESSON_RANDOMBRANCH) {
4427                  $jumptitle = get_string('randombranch', 'lesson');
4428              } elseif ($jumpto == LESSON_CLUSTERJUMP) {
4429                  $jumptitle = get_string('clusterjump', 'lesson');
4430              } else {
4431                  if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) {
4432                      $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>';
4433                  }
4434              }
4435              $jumpnames[$jumpto] = format_string($jumptitle,true);
4436          }
4437  
4438          return $jumpnames[$jumpto];
4439      }
4440  
4441      /**
4442       * Constructor method
4443       * @param object $properties
4444       * @param lesson $lesson
4445       */
4446      public function __construct($properties, lesson $lesson) {
4447          parent::__construct($properties);
4448          $this->lesson = $lesson;
4449      }
4450  
4451      /**
4452       * Returns the score for the attempt
4453       * This may be overridden by page types that require manual grading
4454       * @param array $answers
4455       * @param object $attempt
4456       * @return int
4457       */
4458      public function earned_score($answers, $attempt) {
4459          return $answers[$attempt->answerid]->score;
4460      }
4461  
4462      /**
4463       * This is a callback method that can be override and gets called when ever a page
4464       * is viewed
4465       *
4466       * @param bool $canmanage True if the user has the manage cap
4467       * @param bool $redirect  Optional, default to true. Set to false to avoid redirection and return the page to redirect.
4468       * @return mixed
4469       */
4470      public function callback_on_view($canmanage, $redirect = true) {
4471          return true;
4472      }
4473  
4474      /**
4475       * save editor answers files and update answer record
4476       *
4477       * @param object $context
4478       * @param int $maxbytes
4479       * @param object $answer
4480       * @param object $answereditor
4481       * @param object $responseeditor
4482       */
4483      public function save_answers_files($context, $maxbytes, &$answer, $answereditor = '', $responseeditor = '') {
4484          global $DB;
4485          if (isset($answereditor['itemid'])) {
4486              $answer->answer = file_save_draft_area_files($answereditor['itemid'],
4487                      $context->id, 'mod_lesson', 'page_answers', $answer->id,
4488                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
4489                      $answer->answer, null);
4490              $DB->set_field('lesson_answers', 'answer', $answer->answer, array('id' => $answer->id));
4491          }
4492          if (isset($responseeditor['itemid'])) {
4493              $answer->response = file_save_draft_area_files($responseeditor['itemid'],
4494                      $context->id, 'mod_lesson', 'page_responses', $answer->id,
4495                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
4496                      $answer->response, null);
4497              $DB->set_field('lesson_answers', 'response', $answer->response, array('id' => $answer->id));
4498          }
4499      }
4500  
4501      /**
4502       * Rewrite urls in response and optionality answer of a question answer
4503       *
4504       * @param object $answer
4505       * @param bool $rewriteanswer must rewrite answer
4506       * @return object answer with rewritten urls
4507       */
4508      public static function rewrite_answers_urls($answer, $rewriteanswer = true) {
4509          global $PAGE;
4510  
4511          $context = context_module::instance($PAGE->cm->id);
4512          if ($rewriteanswer) {
4513              $answer->answer = file_rewrite_pluginfile_urls($answer->answer, 'pluginfile.php', $context->id,
4514                      'mod_lesson', 'page_answers', $answer->id);
4515          }
4516          $answer->response = file_rewrite_pluginfile_urls($answer->response, 'pluginfile.php', $context->id,
4517                  'mod_lesson', 'page_responses', $answer->id);
4518  
4519          return $answer;
4520      }
4521  
4522      /**
4523       * Updates a lesson page and its answers within the database
4524       *
4525       * @param object $properties
4526       * @return bool
4527       */
4528      public function update($properties, $context = null, $maxbytes = null) {
4529          global $DB, $PAGE;
4530          $answers  = $this->get_answers();
4531          $properties->id = $this->properties->id;
4532          $properties->lessonid = $this->lesson->id;
4533          if (empty($properties->qoption)) {
4534              $properties->qoption = '0';
4535          }
4536          if (empty($context)) {
4537              $context = $PAGE->context;
4538          }
4539          if ($maxbytes === null) {
4540              $maxbytes = get_user_max_upload_file_size($context);
4541          }
4542          $properties->timemodified = time();
4543          $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $properties->id);
4544          $DB->update_record("lesson_pages", $properties);
4545  
4546          // Trigger an event: page updated.
4547          \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger();
4548  
4549          if ($this->type == self::TYPE_STRUCTURE && $this->get_typeid() != LESSON_PAGE_BRANCHTABLE) {
4550              // These page types have only one answer to save the jump and score.
4551              if (count($answers) > 1) {
4552                  $answer = array_shift($answers);
4553                  foreach ($answers as $a) {
4554                      $DB->delete_records('lesson_answers', array('id' => $a->id));
4555                  }
4556              } else if (count($answers) == 1) {
4557                  $answer = array_shift($answers);
4558              } else {
4559                  $answer = new stdClass;
4560                  $answer->lessonid = $properties->lessonid;
4561                  $answer->pageid = $properties->id;
4562                  $answer->timecreated = time();
4563              }
4564  
4565              $answer->timemodified = time();
4566              if (isset($properties->jumpto[0])) {
4567                  $answer->jumpto = $properties->jumpto[0];
4568              }
4569              if (isset($properties->score[0])) {
4570                  $answer->score = $properties->score[0];
4571              }
4572              if (!empty($answer->id)) {
4573                  $DB->update_record("lesson_answers", $answer->properties());
4574              } else {
4575                  $DB->insert_record("lesson_answers", $answer);
4576              }
4577          } else {
4578              for ($i = 0; $i < count($properties->answer_editor); $i++) {
4579                  if (!array_key_exists($i, $this->answers)) {
4580                      $this->answers[$i] = new stdClass;
4581                      $this->answers[$i]->lessonid = $this->lesson->id;
4582                      $this->answers[$i]->pageid = $this->id;
4583                      $this->answers[$i]->timecreated = $this->timecreated;
4584                      $this->answers[$i]->answer = null;
4585                  }
4586  
4587                  if (isset($properties->answer_editor[$i])) {
4588                      if (is_array($properties->answer_editor[$i])) {
4589                          // Multichoice and true/false pages have an HTML editor.
4590                          $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
4591                          $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
4592                      } else {
4593                          // Branch tables, shortanswer and mumerical pages have only a text field.
4594                          $this->answers[$i]->answer = $properties->answer_editor[$i];
4595                          $this->answers[$i]->answerformat = FORMAT_MOODLE;
4596                      }
4597                  } else {
4598                      // If there is no data posted which means we want to reset the stored values.
4599                      $this->answers[$i]->answer = null;
4600                  }
4601  
4602                  if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
4603                      $this->answers[$i]->response = $properties->response_editor[$i]['text'];
4604                      $this->answers[$i]->responseformat = $properties->response_editor[$i]['format'];
4605                  }
4606  
4607                  if ($this->answers[$i]->answer !== null && $this->answers[$i]->answer !== '') {
4608                      if (isset($properties->jumpto[$i])) {
4609                          $this->answers[$i]->jumpto = $properties->jumpto[$i];
4610                      }
4611                      if ($this->lesson->custom && isset($properties->score[$i])) {
4612                          $this->answers[$i]->score = $properties->score[$i];
4613                      }
4614                      if (!isset($this->answers[$i]->id)) {
4615                          $this->answers[$i]->id = $DB->insert_record("lesson_answers", $this->answers[$i]);
4616                      } else {
4617                          $DB->update_record("lesson_answers", $this->answers[$i]->properties());
4618                      }
4619  
4620                      // Save files in answers and responses.
4621                      if (isset($properties->response_editor[$i])) {
4622                          $this->save_answers_files($context, $maxbytes, $this->answers[$i],
4623                                  $properties->answer_editor[$i], $properties->response_editor[$i]);
4624                      } else {
4625                          $this->save_answers_files($context, $maxbytes, $this->answers[$i],
4626                                  $properties->answer_editor[$i]);
4627                      }
4628  
4629                  } else if (isset($this->answers[$i]->id)) {
4630                      $DB->delete_records('lesson_answers', array('id' => $this->answers[$i]->id));
4631                      unset($this->answers[$i]);
4632                  }
4633              }
4634          }
4635          return true;
4636      }
4637  
4638      /**
4639       * Can be set to true if the page requires a static link to create a new instance
4640       * instead of simply being included in the dropdown
4641       * @param int $previd
4642       * @return bool
4643       */
4644      public function add_page_link($previd) {
4645          return false;
4646      }
4647  
4648      /**
4649       * Returns true if a page has been viewed before
4650       *
4651       * @param array|int $param Either an array of pages that have been seen or the
4652       *                   number of retakes a user has had
4653       * @return bool
4654       */
4655      public function is_unseen($param) {
4656          global $USER, $DB;
4657          if (is_array($param)) {
4658              $seenpages = $param;
4659              return (!array_key_exists($this->properties->id, $seenpages));
4660          } else {
4661              $nretakes = $param;
4662              if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
4663                  return true;
4664              }
4665          }
4666          return false;
4667      }
4668  
4669      /**
4670       * Checks to see if a page has been answered previously
4671       * @param int $nretakes
4672       * @return bool
4673       */
4674      public function is_unanswered($nretakes) {
4675          global $DB, $USER;
4676          if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
4677              return true;
4678          }
4679          return false;
4680      }
4681  
4682      /**
4683       * Creates answers within the database for this lesson_page. Usually only ever
4684       * called when creating a new page instance
4685       * @param object $properties
4686       * @return array
4687       */
4688      public function create_answers($properties) {
4689          global $DB, $PAGE;
4690          // now add the answers
4691          $newanswer = new stdClass;
4692          $newanswer->lessonid = $this->lesson->id;
4693          $newanswer->pageid = $this->properties->id;
4694          $newanswer->timecreated = $this->properties->timecreated;
4695  
4696          $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
4697          $context = context_module::instance($cm->id);
4698  
4699          $answers = array();
4700  
4701          for ($i = 0; $i < ($this->lesson->maxanswers + 1); $i++) {
4702              $answer = clone($newanswer);
4703  
4704              if (isset($properties->answer_editor[$i])) {
4705                  if (is_array($properties->answer_editor[$i])) {
4706                      // Multichoice and true/false pages have an HTML editor.
4707                      $answer->answer = $properties->answer_editor[$i]['text'];
4708                      $answer->answerformat = $properties->answer_editor[$i]['format'];
4709                  } else {
4710                      // Branch tables, shortanswer and mumerical pages have only a text field.
4711                      $answer->answer = $properties->answer_editor[$i];
4712                      $answer->answerformat = FORMAT_MOODLE;
4713                  }
4714              }
4715              if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
4716                  $answer->response = $properties->response_editor[$i]['text'];
4717                  $answer->responseformat = $properties->response_editor[$i]['format'];
4718              }
4719  
4720              if (isset($answer->answer) && $answer->answer != '') {
4721                  if (isset($properties->jumpto[$i])) {
4722                      $answer->jumpto = $properties->jumpto[$i];
4723                  }
4724                  if ($this->lesson->custom && isset($properties->score[$i])) {
4725                      $answer->score = $properties->score[$i];
4726                  }
4727                  $answer->id = $DB->insert_record("lesson_answers", $answer);
4728                  if (isset($properties->response_editor[$i])) {
4729                      $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
4730                              $properties->answer_editor[$i], $properties->response_editor[$i]);
4731                  } else {
4732                      $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
4733                              $properties->answer_editor[$i]);
4734                  }
4735                  $answers[$answer->id] = new lesson_page_answer($answer);
4736              }
4737          }
4738  
4739          $this->answers = $answers;
4740          return $answers;
4741      }
4742  
4743      /**
4744       * This method MUST be overridden by all question page types, or page types that
4745       * wish to score a page.
4746       *
4747       * The structure of result should always be the same so it is a good idea when
4748       * overriding this method on a page type to call
4749       * <code>
4750       * $result = parent::check_answer();
4751       * </code>
4752       * before modifying it as required.
4753       *
4754       * @return stdClass
4755       */
4756      public function check_answer() {
4757          $result = new stdClass;
4758          $result->answerid        = 0;
4759          $result->noanswer        = false;
4760          $result->correctanswer   = false;
4761          $result->isessayquestion = false;   // use this to turn off review button on essay questions
4762          $result->response        = '';
4763          $result->newpageid       = 0;       // stay on the page
4764          $result->studentanswer   = '';      // use this to store student's answer(s) in order to display it on feedback page
4765          $result->studentanswerformat = FORMAT_MOODLE;
4766          $result->userresponse    = null;
4767          $result->feedback        = '';
4768          // Store data that was POSTd by a form. This is currently used to perform any logic after the 1st write to the db
4769          // of the attempt.
4770          $result->postdata        = false;
4771          $result->nodefaultresponse  = false; // Flag for redirecting when default feedback is turned off
4772          $result->inmediatejump = false; // Flag to detect when we should do a jump from the page without further processing.
4773          return $result;
4774      }
4775  
4776      /**
4777       * Do any post persistence processing logic of an attempt. E.g. in cases where we need update file urls in an editor
4778       * and we need to have the id of the stored attempt. Should be overridden in each individual child
4779       * pagetype on a as required basis
4780       *
4781       * @param object $attempt The attempt corresponding to the db record
4782       * @param object $result The result from the 'check_answer' method
4783       * @return array False if nothing to be modified, updated $attempt and $result if update required.
4784       */
4785      public function on_after_write_attempt($attempt, $result) {
4786          return [false, false];
4787      }
4788  
4789      /**
4790       * True if the page uses a custom option
4791       *
4792       * Should be override and set to true if the page uses a custom option.
4793       *
4794       * @return bool
4795       */
4796      public function has_option() {
4797          return false;
4798      }
4799  
4800      /**
4801       * Returns the maximum number of answers for this page given the maximum number
4802       * of answers permitted by the lesson.
4803       *
4804       * @param int $default
4805       * @return int
4806       */
4807      public function max_answers($default) {
4808          return $default;
4809      }
4810  
4811      /**
4812       * Returns the properties of this lesson page as an object
4813       * @return stdClass;
4814       */
4815      public function properties() {
4816          $properties = clone($this->properties);
4817          if ($this->answers === null) {
4818              $this->get_answers();
4819          }
4820          if (count($this->answers)>0) {
4821              $count = 0;
4822              $qtype = $properties->qtype;
4823              foreach ($this->answers as $answer) {
4824                  $properties->{'answer_editor['.$count.']'} = array('text' => $answer->answer, 'format' => $answer->answerformat);
4825                  if ($qtype != LESSON_PAGE_MATCHING) {
4826                      $properties->{'response_editor['.$count.']'} = array('text' => $answer->response, 'format' => $answer->responseformat);
4827                  } else {
4828                      $properties->{'response_editor['.$count.']'} = $answer->response;
4829                  }
4830                  $properties->{'jumpto['.$count.']'} = $answer->jumpto;
4831                  $properties->{'score['.$count.']'} = $answer->score;
4832                  $count++;
4833              }
4834          }
4835          return $properties;
4836      }
4837  
4838      /**
4839       * Returns an array of options to display when choosing the jumpto for a page/answer
4840       * @static
4841       * @param int $pageid
4842       * @param lesson $lesson
4843       * @return array
4844       */
4845      public static function get_jumptooptions($pageid, lesson $lesson) {
4846          global $DB;
4847          $jump = array();
4848          $jump[0] = get_string("thispage", "lesson");
4849          $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson");
4850          $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson");
4851          $jump[LESSON_EOL] = get_string("endoflesson", "lesson");
4852  
4853          if ($pageid == 0) {
4854              return $jump;
4855          }
4856  
4857          $pages = $lesson->load_all_pages();
4858          if ($pages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))) {
4859              $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson");
4860              $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson");
4861          }
4862          if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) {
4863              $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson");
4864          }
4865          if (!optional_param('firstpage', 0, PARAM_INT)) {
4866              $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0));
4867              while (true) {
4868                  if ($apageid) {
4869                      $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid));
4870                      $jump[$apageid] = strip_tags(format_string($title,true));
4871                      $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid));
4872                  } else {
4873                      // last page reached
4874                      break;
4875                  }
4876              }
4877          }
4878          return $jump;
4879      }
4880      /**
4881       * Returns the contents field for the page properly formatted and with plugin
4882       * file url's converted
4883       * @return string
4884       */
4885      public function get_contents() {
4886          global $PAGE;
4887          if (!empty($this->properties->contents)) {
4888              if (!isset($this->properties->contentsformat)) {
4889                  $this->properties->contentsformat = FORMAT_HTML;
4890              }
4891              $context = context_module::instance($PAGE->cm->id);
4892              $contents = file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'mod_lesson',
4893                                                       'page_contents', $this->properties->id);  // Must do this BEFORE format_text()!
4894              return format_text($contents, $this->properties->contentsformat,
4895                                 array('context' => $context, 'noclean' => true,
4896                                       'overflowdiv' => true));  // Page edit is marked with XSS, we want all content here.
4897          } else {
4898              return '';
4899          }
4900      }
4901  
4902      /**
4903       * Set to true if this page should display in the menu block
4904       * @return bool
4905       */
4906      protected function get_displayinmenublock() {
4907          return false;
4908      }
4909  
4910      /**
4911       * Get the string that describes the options of this page type
4912       * @return string
4913       */
4914      public function option_description_string() {
4915          return '';
4916      }
4917  
4918      /**
4919       * Updates a table with the answers for this page
4920       * @param html_table $table
4921       * @return html_table
4922       */
4923      public function display_answers(html_table $table) {
4924          $answers = $this->get_answers();
4925          $i = 1;
4926          foreach ($answers as $answer) {
4927              $cells = array();
4928              $cells[] = '<label>' . get_string('jump', 'lesson') . ' ' . $i . '</label>:';
4929              $cells[] = $this->get_jump_name($answer->jumpto);
4930              $table->data[] = new html_table_row($cells);
4931              if ($i === 1){
4932                  $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
4933              }
4934              $i++;
4935          }
4936          return $table;
4937      }
4938  
4939      /**
4940       * Determines if this page should be grayed out on the management/report screens
4941       * @return int 0 or 1
4942       */
4943      protected function get_grayout() {
4944          return 0;
4945      }
4946  
4947      /**
4948       * Adds stats for this page to the &pagestats object. This should be defined
4949       * for all page types that grade
4950       * @param array $pagestats
4951       * @param int $tries
4952       * @return bool
4953       */
4954      public function stats(array &$pagestats, $tries) {
4955          return true;
4956      }
4957  
4958      /**
4959       * Formats the answers of this page for a report
4960       *
4961       * @param object $answerpage
4962       * @param object $answerdata
4963       * @param object $useranswer
4964       * @param array $pagestats
4965       * @param int $i Count of first level answers
4966       * @param int $n Count of second level answers
4967       * @return object The answer page for this
4968       */
4969      public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
4970          $answers = $this->get_answers();
4971          $formattextdefoptions = new stdClass;
4972          $formattextdefoptions->para = false;  //I'll use it widely in this page
4973          foreach ($answers as $answer) {
4974              $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto));
4975              $answerdata->answers[] = array($data, "");
4976              $answerpage->answerdata = $answerdata;
4977          }
4978          return $answerpage;
4979      }
4980  
4981      /**
4982       * Gets an array of the jumps used by the answers of this page
4983       *
4984       * @return array
4985       */
4986      public function get_jumps() {
4987          global $DB;
4988          $jumps = array();
4989          $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id);
4990          if ($answers = $this->get_answers()) {
4991              foreach ($answers as $answer) {
4992                  $jumps[] = $this->get_jump_name($answer->jumpto);
4993              }
4994          } else {
4995              $jumps[] = $this->get_jump_name($this->properties->nextpageid);
4996          }
4997          return $jumps;
4998      }
4999      /**
5000       * Informs whether this page type require manual grading or not
5001       * @return bool
5002       */
5003      public function requires_manual_grading() {
5004          return false;
5005      }
5006  
5007      /**
5008       * A callback method that allows a page to override the next page a user will
5009       * see during when this page is being completed.
5010       * @return false|int
5011       */
5012      public function override_next_page() {
5013          return false;
5014      }
5015  
5016      /**
5017       * This method is used to determine if this page is a valid page
5018       *
5019       * @param array $validpages
5020       * @param array $pageviews
5021       * @return int The next page id to check
5022       */
5023      public function valid_page_and_view(&$validpages, &$pageviews) {
5024          $validpages[$this->properties->id] = 1;
5025          return $this->properties->nextpageid;
5026      }
5027  
5028      /**
5029       * Get files from the page area file.
5030       *
5031       * @param bool $includedirs whether or not include directories
5032       * @param int $updatedsince return files updated since this time
5033       * @return array list of stored_file objects
5034       * @since  Moodle 3.2
5035       */
5036      public function get_files($includedirs = true, $updatedsince = 0) {
5037          $fs = get_file_storage();
5038          return $fs->get_area_files($this->lesson->context->id, 'mod_lesson', 'page_contents', $this->properties->id,
5039                                      'itemid, filepath, filename', $includedirs, $updatedsince);
5040      }
5041  
5042      /**
5043       * Make updates to the form data if required.
5044       *
5045       * @since Moodle 3.7
5046       * @param stdClass $data The form data to update.
5047       * @return stdClass The updated fom data.
5048       */
5049      public function update_form_data(stdClass $data) : stdClass {
5050          return $data;
5051      }
5052  }
5053  
5054  
5055  
5056  /**
5057   * Class used to represent an answer to a page
5058   *
5059   * @property int $id The ID of this answer in the database
5060   * @property int $lessonid The ID of the lesson this answer belongs to
5061   * @property int $pageid The ID of the page this answer belongs to
5062   * @property int $jumpto Identifies where the user goes upon completing a page with this answer
5063   * @property int $grade The grade this answer is worth
5064   * @property int $score The score this answer will give
5065   * @property int $flags Used to store options for the answer
5066   * @property int $timecreated A timestamp of when the answer was created
5067   * @property int $timemodified A timestamp of when the answer was modified
5068   * @property string $answer The answer itself
5069   * @property string $response The response the user sees if selecting this answer
5070   *
5071   * @copyright  2009 Sam Hemelryk
5072   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5073   */
5074  class lesson_page_answer extends lesson_base {
5075  
5076      /**
5077       * Loads an page answer from the DB
5078       *
5079       * @param int $id
5080       * @return lesson_page_answer
5081       */
5082      public static function load($id) {
5083          global $DB;
5084          $answer = $DB->get_record("lesson_answers", array("id" => $id));
5085          return new lesson_page_answer($answer);
5086      }
5087  
5088      /**
5089       * Given an object of properties and a page created answer(s) and saves them
5090       * in the database.
5091       *
5092       * @param stdClass $properties
5093       * @param lesson_page $page
5094       * @return array
5095       */
5096      public static function create($properties, lesson_page $page) {
5097          return $page->create_answers($properties);
5098      }
5099  
5100      /**
5101       * Get files from the answer area file.
5102       *
5103       * @param bool $includedirs whether or not include directories
5104       * @param int $updatedsince return files updated since this time
5105       * @return array list of stored_file objects
5106       * @since  Moodle 3.2
5107       */
5108      public function get_files($includedirs = true, $updatedsince = 0) {
5109  
5110          $lesson = lesson::load($this->properties->lessonid);
5111          $fs = get_file_storage();
5112          $answerfiles = $fs->get_area_files($lesson->context->id, 'mod_lesson', 'page_answers', $this->properties->id,
5113                                              'itemid, filepath, filename', $includedirs, $updatedsince);
5114          $responsefiles = $fs->get_area_files($lesson->context->id, 'mod_lesson', 'page_responses', $this->properties->id,
5115                                              'itemid, filepath, filename', $includedirs, $updatedsince);
5116          return array_merge($answerfiles, $responsefiles);
5117      }
5118  
5119  }
5120  
5121  /**
5122   * A management class for page types
5123   *
5124   * This class is responsible for managing the different pages. A manager object can
5125   * be retrieved by calling the following line of code:
5126   * <code>
5127   * $manager  = lesson_page_type_manager::get($lesson);
5128   * </code>
5129   * The first time the page type manager is retrieved the it includes all of the
5130   * different page types located in mod/lesson/pagetypes.
5131   *
5132   * @copyright  2009 Sam Hemelryk
5133   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5134   */
5135  class lesson_page_type_manager {
5136  
5137      /**
5138       * An array of different page type classes
5139       * @var array
5140       */
5141      protected $types = array();
5142  
5143      /**
5144       * Retrieves the lesson page type manager object
5145       *
5146       * If the object hasn't yet been created it is created here.
5147       *
5148       * @staticvar lesson_page_type_manager $pagetypemanager
5149       * @param lesson $lesson
5150       * @return lesson_page_type_manager
5151       */
5152      public static function get(lesson $lesson) {
5153          static $pagetypemanager;
5154          if (!($pagetypemanager instanceof lesson_page_type_manager)) {
5155              $pagetypemanager = new lesson_page_type_manager();
5156              $pagetypemanager->load_lesson_types($lesson);
5157          }
5158          return $pagetypemanager;
5159      }
5160  
5161      /**
5162       * Finds and loads all lesson page types in mod/lesson/pagetypes
5163       *
5164       * @param lesson $lesson
5165       */
5166      public function load_lesson_types(lesson $lesson) {
5167          global $CFG;
5168          $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/';
5169          $dir = dir($basedir);
5170          while (false !== ($entry = $dir->read())) {
5171              if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) {
5172                  continue;
5173              }
5174              require_once($basedir.$entry);
5175              $class = 'lesson_page_type_'.strtok($entry,'.');
5176              if (class_exists($class)) {
5177                  $pagetype = new $class(new stdClass, $lesson);
5178                  $this->types[$pagetype->typeid] = $pagetype;
5179              }
5180          }
5181  
5182      }
5183  
5184      /**
5185       * Returns an array of strings to describe the loaded page types
5186       *
5187       * @param int $type Can be used to return JUST the string for the requested type
5188       * @return array
5189       */
5190      public function get_page_type_strings($type=null, $special=true) {
5191          $types = array();
5192          foreach ($this->types as $pagetype) {
5193              if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) {
5194                  $types[$pagetype->typeid] = $pagetype->typestring;
5195              }
5196          }
5197          return $types;
5198      }
5199  
5200      /**
5201       * Returns the basic string used to identify a page type provided with an id
5202       *
5203       * This string can be used to instantiate or identify the page type class.
5204       * If the page type id is unknown then 'unknown' is returned
5205       *
5206       * @param int $id
5207       * @return string
5208       */
5209      public function get_page_type_idstring($id) {
5210          foreach ($this->types as $pagetype) {
5211              if ((int)$pagetype->typeid === (int)$id) {
5212                  return $pagetype->idstring;
5213              }
5214          }
5215          return 'unknown';
5216      }
5217  
5218      /**
5219       * Loads a page for the provided lesson given it's id
5220       *
5221       * This function loads a page from the lesson when given both the lesson it belongs
5222       * to as well as the page's id.
5223       * If the page doesn't exist an error is thrown
5224       *
5225       * @param int $pageid The id of the page to load
5226       * @param lesson $lesson The lesson the page belongs to
5227       * @return lesson_page A class that extends lesson_page
5228       */
5229      public function load_page($pageid, lesson $lesson) {
5230          global $DB;
5231          if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) {
5232              print_error('cannotfindpages', 'lesson');
5233          }
5234          $pagetype = get_class($this->types[$page->qtype]);
5235          $page = new $pagetype($page, $lesson);
5236          return $page;
5237      }
5238  
5239      /**
5240       * This function detects errors in the ordering between 2 pages and updates the page records.
5241       *
5242       * @param stdClass $page1 Either the first of 2 pages or null if the $page2 param is the first in the list.
5243       * @param stdClass $page1 Either the second of 2 pages or null if the $page1 param is the last in the list.
5244       */
5245      protected function check_page_order($page1, $page2) {
5246          global $DB;
5247          if (empty($page1)) {
5248              if ($page2->prevpageid != 0) {
5249                  debugging("***prevpageid of page " . $page2->id . " set to 0***");
5250                  $page2->prevpageid = 0;
5251                  $DB->set_field("lesson_pages", "prevpageid", 0, array("id" => $page2->id));
5252              }
5253          } else if (empty($page2)) {
5254              if ($page1->nextpageid != 0) {
5255                  debugging("***nextpageid of page " . $page1->id . " set to 0***");
5256                  $page1->nextpageid = 0;
5257                  $DB->set_field("lesson_pages", "nextpageid", 0, array("id" => $page1->id));
5258              }
5259          } else {
5260              if ($page1->nextpageid != $page2->id) {
5261                  debugging("***nextpageid of page " . $page1->id . " set to " . $page2->id . "***");
5262                  $page1->nextpageid = $page2->id;
5263                  $DB->set_field("lesson_pages", "nextpageid", $page2->id, array("id" => $page1->id));
5264              }
5265              if ($page2->prevpageid != $page1->id) {
5266                  debugging("***prevpageid of page " . $page2->id . " set to " . $page1->id . "***");
5267                  $page2->prevpageid = $page1->id;
5268                  $DB->set_field("lesson_pages", "prevpageid", $page1->id, array("id" => $page2->id));
5269              }
5270          }
5271      }
5272  
5273      /**
5274       * This function loads ALL pages that belong to the lesson.
5275       *
5276       * @param lesson $lesson
5277       * @return array An array of lesson_page_type_*
5278       */
5279      public function load_all_pages(lesson $lesson) {
5280          global $DB;
5281          if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) {
5282              return array(); // Records returned empty.
5283          }
5284          foreach ($pages as $key=>$page) {
5285              $pagetype = get_class($this->types[$page->qtype]);
5286              $pages[$key] = new $pagetype($page, $lesson);
5287          }
5288  
5289          $orderedpages = array();
5290          $lastpageid = 0;
5291          $morepages = true;
5292          while ($morepages) {
5293              $morepages = false;
5294              foreach ($pages as $page) {
5295                  if ((int)$page->prevpageid === (int)$lastpageid) {
5296                      // Check for errors in page ordering and fix them on the fly.
5297                      $prevpage = null;
5298                      if ($lastpageid !== 0) {
5299                          $prevpage = $orderedpages[$lastpageid];
5300                      }
5301                      $this->check_page_order($prevpage, $page);
5302                      $morepages = true;
5303                      $orderedpages[$page->id] = $page;
5304                      unset($pages[$page->id]);
5305                      $lastpageid = $page->id;
5306                      if ((int)$page->nextpageid===0) {
5307                          break 2;
5308                      } else {
5309                          break 1;
5310                      }
5311                  }
5312              }
5313          }
5314  
5315          // Add remaining pages and fix the nextpageid links for each page.
5316          foreach ($pages as $page) {
5317              // Check for errors in page ordering and fix them on the fly.
5318              $prevpage = null;
5319              if ($lastpageid !== 0) {
5320                  $prevpage = $orderedpages[$lastpageid];
5321              }
5322              $this->check_page_order($prevpage, $page);
5323              $orderedpages[$page->id] = $page;
5324              unset($pages[$page->id]);
5325              $lastpageid = $page->id;
5326          }
5327  
5328          if ($lastpageid !== 0) {
5329              $this->check_page_order($orderedpages[$lastpageid], null);
5330          }
5331  
5332          return $orderedpages;
5333      }
5334  
5335      /**
5336       * Fetches an mform that can be used to create/edit an page
5337       *
5338       * @param int $type The id for the page type
5339       * @param array $arguments Any arguments to pass to the mform
5340       * @return lesson_add_page_form_base
5341       */
5342      public function get_page_form($type, $arguments) {
5343          $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type);
5344          if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') {
5345              debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER);
5346              $class = 'lesson_add_page_form_selection';
5347          } else if ($class === 'lesson_add_page_form_unknown') {
5348              $class = 'lesson_add_page_form_selection';
5349          }
5350          return new $class(null, $arguments);
5351      }
5352  
5353      /**
5354       * Returns an array of links to use as add page links
5355       * @param int $previd The id of the previous page
5356       * @return array
5357       */
5358      public function get_add_page_type_links($previd) {
5359          global $OUTPUT;
5360  
5361          $links = array();
5362  
5363          foreach ($this->types as $key=>$type) {
5364              if ($link = $type->add_page_link($previd)) {
5365                  $links[$key] = $link;
5366              }
5367          }
5368  
5369          return $links;
5370      }
5371  }