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.