[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/lesson/ -> locallib.php (source)

   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  //////////////////////////////////////////////////////////////////////////////////////
  65  /// Any other lesson functions go here.  Each of them must have a name that
  66  /// starts with lesson_
  67  
  68  /**
  69   * Checks to see if a LESSON_CLUSTERJUMP or
  70   * a LESSON_UNSEENBRANCHPAGE is used in a lesson.
  71   *
  72   * This function is only executed when a teacher is
  73   * checking the navigation for a lesson.
  74   *
  75   * @param stdClass $lesson Id of the lesson that is to be checked.
  76   * @return boolean True or false.
  77   **/
  78  function lesson_display_teacher_warning($lesson) {
  79      global $DB;
  80  
  81      // get all of the lesson answers
  82      $params = array ("lessonid" => $lesson->id);
  83      if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) {
  84          // no answers, then not using cluster or unseen
  85          return false;
  86      }
  87      // just check for the first one that fulfills the requirements
  88      foreach ($lessonanswers as $lessonanswer) {
  89          if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) {
  90              return true;
  91          }
  92      }
  93  
  94      // if no answers use either of the two jumps
  95      return false;
  96  }
  97  
  98  /**
  99   * Interprets the LESSON_UNSEENBRANCHPAGE jump.
 100   *
 101   * will return the pageid of a random unseen page that is within a branch
 102   *
 103   * @param lesson $lesson
 104   * @param int $userid Id of the user.
 105   * @param int $pageid Id of the page from which we are jumping.
 106   * @return int Id of the next page.
 107   **/
 108  function lesson_unseen_question_jump($lesson, $user, $pageid) {
 109      global $DB;
 110  
 111      // get the number of retakes
 112      if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) {
 113          $retakes = 0;
 114      }
 115  
 116      // get all the lesson_attempts aka what the user has seen
 117      if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) {
 118          foreach($viewedpages as $viewed) {
 119              $seenpages[] = $viewed->pageid;
 120          }
 121      } else {
 122          $seenpages = array();
 123      }
 124  
 125      // get the lesson pages
 126      $lessonpages = $lesson->load_all_pages();
 127  
 128      if ($pageid == LESSON_UNSEENBRANCHPAGE) {  // this only happens when a student leaves in the middle of an unseen question within a branch series
 129          $pageid = $seenpages[0];  // just change the pageid to the last page viewed inside the branch table
 130      }
 131  
 132      // go up the pages till branch table
 133      while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
 134          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 135              break;
 136          }
 137          $pageid = $lessonpages[$pageid]->prevpageid;
 138      }
 139  
 140      $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
 141  
 142      // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array
 143      $unseen = array();
 144      foreach($pagesinbranch as $page) {
 145          if (!in_array($page->id, $seenpages)) {
 146              $unseen[] = $page->id;
 147          }
 148      }
 149  
 150      if(count($unseen) == 0) {
 151          if(isset($pagesinbranch)) {
 152              $temp = end($pagesinbranch);
 153              $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL
 154          } else {
 155              // there are no pages inside the branch, so return the next page
 156              $nextpage = $lessonpages[$pageid]->nextpageid;
 157          }
 158          if ($nextpage == 0) {
 159              return LESSON_EOL;
 160          } else {
 161              return $nextpage;
 162          }
 163      } else {
 164          return $unseen[rand(0, count($unseen)-1)];  // returns a random page id for the next page
 165      }
 166  }
 167  
 168  /**
 169   * Handles the unseen branch table jump.
 170   *
 171   * @param lesson $lesson
 172   * @param int $userid User id.
 173   * @return int Will return the page id of a branch table or end of lesson
 174   **/
 175  function lesson_unseen_branch_jump($lesson, $userid) {
 176      global $DB;
 177  
 178      if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) {
 179          $retakes = 0;
 180      }
 181  
 182      $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $retakes);
 183      if (!$seenbranches = $DB->get_records_select("lesson_branch", "lessonid = :lessonid AND userid = :userid AND retry = :retry", $params,
 184                  "timeseen DESC")) {
 185          print_error('cannotfindrecords', 'lesson');
 186      }
 187  
 188      // get the lesson pages
 189      $lessonpages = $lesson->load_all_pages();
 190  
 191      // this loads all the viewed branch tables into $seen until it finds the branch table with the flag
 192      // which is the branch table that starts the unseenbranch function
 193      $seen = array();
 194      foreach ($seenbranches as $seenbranch) {
 195          if (!$seenbranch->flag) {
 196              $seen[$seenbranch->pageid] = $seenbranch->pageid;
 197          } else {
 198              $start = $seenbranch->pageid;
 199              break;
 200          }
 201      }
 202      // this function searches through the lesson pages to find all the branch tables
 203      // that follow the flagged branch table
 204      $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table
 205      $branchtables = array();
 206      while ($pageid != 0) {  // grab all of the branch table till eol
 207          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 208              $branchtables[] = $lessonpages[$pageid]->id;
 209          }
 210          $pageid = $lessonpages[$pageid]->nextpageid;
 211      }
 212      $unseen = array();
 213      foreach ($branchtables as $branchtable) {
 214          // load all of the unseen branch tables into unseen
 215          if (!array_key_exists($branchtable, $seen)) {
 216              $unseen[] = $branchtable;
 217          }
 218      }
 219      if (count($unseen) > 0) {
 220          return $unseen[rand(0, count($unseen)-1)];  // returns a random page id for the next page
 221      } else {
 222          return LESSON_EOL;  // has viewed all of the branch tables
 223      }
 224  }
 225  
 226  /**
 227   * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE).
 228   *
 229   * @param lesson $lesson
 230   * @param int $pageid The id of the page that we are jumping from (?)
 231   * @return int The pageid of a random page that is within a branch table
 232   **/
 233  function lesson_random_question_jump($lesson, $pageid) {
 234      global $DB;
 235  
 236      // get the lesson pages
 237      $params = array ("lessonid" => $lesson->id);
 238      if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) {
 239          print_error('cannotfindpages', 'lesson');
 240      }
 241  
 242      // go up the pages till branch table
 243      while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
 244  
 245          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 246              break;
 247          }
 248          $pageid = $lessonpages[$pageid]->prevpageid;
 249      }
 250  
 251      // get the pages within the branch
 252      $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
 253  
 254      if(count($pagesinbranch) == 0) {
 255          // there are no pages inside the branch, so return the next page
 256          return $lessonpages[$pageid]->nextpageid;
 257      } else {
 258          return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id;  // returns a random page id for the next page
 259      }
 260  }
 261  
 262  /**
 263   * Calculates a user's grade for a lesson.
 264   *
 265   * @param object $lesson The lesson that the user is taking.
 266   * @param int $retries The attempt number.
 267   * @param int $userid Id of the user (optional, default current user).
 268   * @return object { nquestions => number of questions answered
 269                      attempts => number of question attempts
 270                      total => max points possible
 271                      earned => points earned by student
 272                      grade => calculated percentage grade
 273                      nmanual => number of manually graded questions
 274                      manualpoints => point value for manually graded questions }
 275   */
 276  function lesson_grade($lesson, $ntries, $userid = 0) {
 277      global $USER, $DB;
 278  
 279      if (empty($userid)) {
 280          $userid = $USER->id;
 281      }
 282  
 283      // Zero out everything
 284      $ncorrect     = 0;
 285      $nviewed      = 0;
 286      $score        = 0;
 287      $nmanual      = 0;
 288      $manualpoints = 0;
 289      $thegrade     = 0;
 290      $nquestions   = 0;
 291      $total        = 0;
 292      $earned       = 0;
 293  
 294      $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries);
 295      if ($useranswers = $DB->get_records_select("lesson_attempts",  "lessonid = :lessonid AND
 296              userid = :userid AND retry = :retry", $params, "timeseen")) {
 297          // group each try with its page
 298          $attemptset = array();
 299          foreach ($useranswers as $useranswer) {
 300              $attemptset[$useranswer->pageid][] = $useranswer;
 301          }
 302  
 303          // Drop all attempts that go beyond max attempts for the lesson
 304          foreach ($attemptset as $key => $set) {
 305              $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts);
 306          }
 307  
 308          // get only the pages and their answers that the user answered
 309          list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset));
 310          array_unshift($parameters, $lesson->id);
 311          $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters);
 312          $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters);
 313  
 314          // Number of pages answered
 315          $nquestions = count($pages);
 316  
 317          foreach ($attemptset as $attempts) {
 318              $page = lesson_page::load($pages[end($attempts)->pageid], $lesson);
 319              if ($lesson->custom) {
 320                  $attempt = end($attempts);
 321                  // If essay question, handle it, otherwise add to score
 322                  if ($page->requires_manual_grading()) {
 323                      $useranswerobj = unserialize($attempt->useranswer);
 324                      if (isset($useranswerobj->score)) {
 325                          $earned += $useranswerobj->score;
 326                      }
 327                      $nmanual++;
 328                      $manualpoints += $answers[$attempt->answerid]->score;
 329                  } else if (!empty($attempt->answerid)) {
 330                      $earned += $page->earned_score($answers, $attempt);
 331                  }
 332              } else {
 333                  foreach ($attempts as $attempt) {
 334                      $earned += $attempt->correct;
 335                  }
 336                  $attempt = end($attempts); // doesn't matter which one
 337                  // If essay question, increase numbers
 338                  if ($page->requires_manual_grading()) {
 339                      $nmanual++;
 340                      $manualpoints++;
 341                  }
 342              }
 343              // Number of times answered
 344              $nviewed += count($attempts);
 345          }
 346  
 347          if ($lesson->custom) {
 348              $bestscores = array();
 349              // Find the highest possible score per page to get our total
 350              foreach ($answers as $answer) {
 351                  if(!isset($bestscores[$answer->pageid])) {
 352                      $bestscores[$answer->pageid] = $answer->score;
 353                  } else if ($bestscores[$answer->pageid] < $answer->score) {
 354                      $bestscores[$answer->pageid] = $answer->score;
 355                  }
 356              }
 357              $total = array_sum($bestscores);
 358          } else {
 359              // Check to make sure the student has answered the minimum questions
 360              if ($lesson->minquestions and $nquestions < $lesson->minquestions) {
 361                  // Nope, increase number viewed by the amount of unanswered questions
 362                  $total =  $nviewed + ($lesson->minquestions - $nquestions);
 363              } else {
 364                  $total = $nviewed;
 365              }
 366          }
 367      }
 368  
 369      if ($total) { // not zero
 370          $thegrade = round(100 * $earned / $total, 5);
 371      }
 372  
 373      // Build the grade information object
 374      $gradeinfo               = new stdClass;
 375      $gradeinfo->nquestions   = $nquestions;
 376      $gradeinfo->attempts     = $nviewed;
 377      $gradeinfo->total        = $total;
 378      $gradeinfo->earned       = $earned;
 379      $gradeinfo->grade        = $thegrade;
 380      $gradeinfo->nmanual      = $nmanual;
 381      $gradeinfo->manualpoints = $manualpoints;
 382  
 383      return $gradeinfo;
 384  }
 385  
 386  /**
 387   * Determines if a user can view the left menu.  The determining factor
 388   * is whether a user has a grade greater than or equal to the lesson setting
 389   * of displayleftif
 390   *
 391   * @param object $lesson Lesson object of the current lesson
 392   * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged
 393   **/
 394  function lesson_displayleftif($lesson) {
 395      global $CFG, $USER, $DB;
 396  
 397      if (!empty($lesson->displayleftif)) {
 398          // get the current user's max grade for this lesson
 399          $params = array ("userid" => $USER->id, "lessonid" => $lesson->id);
 400          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)) {
 401              if ($maxgrade->maxgrade < $lesson->displayleftif) {
 402                  return 0;  // turn off the displayleft
 403              }
 404          } else {
 405              return 0; // no grades
 406          }
 407      }
 408  
 409      // if we get to here, keep the original state of displayleft lesson setting
 410      return $lesson->displayleft;
 411  }
 412  
 413  /**
 414   *
 415   * @param $cm
 416   * @param $lesson
 417   * @param $page
 418   * @return unknown_type
 419   */
 420  function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) {
 421      $bc = lesson_menu_block_contents($cm->id, $lesson);
 422      if (!empty($bc)) {
 423          $regions = $page->blocks->get_regions();
 424          $firstregion = reset($regions);
 425          $page->blocks->add_fake_block($bc, $firstregion);
 426      }
 427  
 428      $bc = lesson_mediafile_block_contents($cm->id, $lesson);
 429      if (!empty($bc)) {
 430          $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
 431      }
 432  
 433      if (!empty($timer)) {
 434          $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page);
 435          if (!empty($bc)) {
 436              $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
 437          }
 438      }
 439  }
 440  
 441  /**
 442   * If there is a media file associated with this
 443   * lesson, return a block_contents that displays it.
 444   *
 445   * @param int $cmid Course Module ID for this lesson
 446   * @param object $lesson Full lesson record object
 447   * @return block_contents
 448   **/
 449  function lesson_mediafile_block_contents($cmid, $lesson) {
 450      global $OUTPUT;
 451      if (empty($lesson->mediafile)) {
 452          return null;
 453      }
 454  
 455      $options = array();
 456      $options['menubar'] = 0;
 457      $options['location'] = 0;
 458      $options['left'] = 5;
 459      $options['top'] = 5;
 460      $options['scrollbars'] = 1;
 461      $options['resizable'] = 1;
 462      $options['width'] = $lesson->mediawidth;
 463      $options['height'] = $lesson->mediaheight;
 464  
 465      $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid);
 466      $action = new popup_action('click', $link, 'lessonmediafile', $options);
 467      $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson')));
 468  
 469      $bc = new block_contents();
 470      $bc->title = get_string('linkedmedia', 'lesson');
 471      $bc->attributes['class'] = 'mediafile block';
 472      $bc->content = $content;
 473  
 474      return $bc;
 475  }
 476  
 477  /**
 478   * If a timed lesson and not a teacher, then
 479   * return a block_contents containing the clock.
 480   *
 481   * @param int $cmid Course Module ID for this lesson
 482   * @param object $lesson Full lesson record object
 483   * @param object $timer Full timer record object
 484   * @return block_contents
 485   **/
 486  function lesson_clock_block_contents($cmid, $lesson, $timer, $page) {
 487      // Display for timed lessons and for students only
 488      $context = context_module::instance($cmid);
 489      if ($lesson->timelimit == 0 || has_capability('mod/lesson:manage', $context)) {
 490          return null;
 491      }
 492  
 493      $content = '<div id="lesson-timer">';
 494      $content .=  $lesson->time_remaining($timer->starttime);
 495      $content .= '</div>';
 496  
 497      $clocksettings = array('starttime' => $timer->starttime, 'servertime' => time(), 'testlength' => $lesson->timelimit);
 498      $page->requires->data_for_js('clocksettings', $clocksettings, true);
 499      $page->requires->strings_for_js(array('timeisup'), 'lesson');
 500      $page->requires->js('/mod/lesson/timer.js');
 501      $page->requires->js_init_call('show_clock');
 502  
 503      $bc = new block_contents();
 504      $bc->title = get_string('timeremaining', 'lesson');
 505      $bc->attributes['class'] = 'clock block';
 506      $bc->content = $content;
 507  
 508      return $bc;
 509  }
 510  
 511  /**
 512   * If left menu is turned on, then this will
 513   * print the menu in a block
 514   *
 515   * @param int $cmid Course Module ID for this lesson
 516   * @param lesson $lesson Full lesson record object
 517   * @return void
 518   **/
 519  function lesson_menu_block_contents($cmid, $lesson) {
 520      global $CFG, $DB;
 521  
 522      if (!$lesson->displayleft) {
 523          return null;
 524      }
 525  
 526      $pages = $lesson->load_all_pages();
 527      foreach ($pages as $page) {
 528          if ((int)$page->prevpageid === 0) {
 529              $pageid = $page->id;
 530              break;
 531          }
 532      }
 533      $currentpageid = optional_param('pageid', $pageid, PARAM_INT);
 534  
 535      if (!$pageid || !$pages) {
 536          return null;
 537      }
 538  
 539      $content = '<a href="#maincontent" class="skip">'.get_string('skip', 'lesson')."</a>\n<div class=\"menuwrapper\">\n<ul>\n";
 540  
 541      while ($pageid != 0) {
 542          $page = $pages[$pageid];
 543  
 544          // Only process branch tables with display turned on
 545          if ($page->displayinmenublock && $page->display) {
 546              if ($page->id == $currentpageid) {
 547                  $content .= '<li class="selected">'.format_string($page->title,true)."</li>\n";
 548              } else {
 549                  $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";
 550              }
 551  
 552          }
 553          $pageid = $page->nextpageid;
 554      }
 555      $content .= "</ul>\n</div>\n";
 556  
 557      $bc = new block_contents();
 558      $bc->title = get_string('lessonmenu', 'lesson');
 559      $bc->attributes['class'] = 'menu block';
 560      $bc->content = $content;
 561  
 562      return $bc;
 563  }
 564  
 565  /**
 566   * Adds header buttons to the page for the lesson
 567   *
 568   * @param object $cm
 569   * @param object $context
 570   * @param bool $extraeditbuttons
 571   * @param int $lessonpageid
 572   */
 573  function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $lessonpageid=null) {
 574      global $CFG, $PAGE, $OUTPUT;
 575      if (has_capability('mod/lesson:edit', $context) && $extraeditbuttons) {
 576          if ($lessonpageid === null) {
 577              print_error('invalidpageid', 'lesson');
 578          }
 579          if (!empty($lessonpageid) && $lessonpageid != LESSON_EOL) {
 580              $url = new moodle_url('/mod/lesson/editpage.php', array(
 581                  'id'       => $cm->id,
 582                  'pageid'   => $lessonpageid,
 583                  'edit'     => 1,
 584                  'returnto' => $PAGE->url->out(false)
 585              ));
 586              $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
 587          }
 588      }
 589  }
 590  
 591  /**
 592   * This is a function used to detect media types and generate html code.
 593   *
 594   * @global object $CFG
 595   * @global object $PAGE
 596   * @param object $lesson
 597   * @param object $context
 598   * @return string $code the html code of media
 599   */
 600  function lesson_get_media_html($lesson, $context) {
 601      global $CFG, $PAGE, $OUTPUT;
 602      require_once("$CFG->libdir/resourcelib.php");
 603  
 604      // get the media file link
 605      if (strpos($lesson->mediafile, '://') !== false) {
 606          $url = new moodle_url($lesson->mediafile);
 607      } else {
 608          // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder
 609          $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/'));
 610      }
 611      $title = $lesson->mediafile;
 612  
 613      $clicktoopen = html_writer::link($url, get_string('download'));
 614  
 615      $mimetype = resourcelib_guess_url_mimetype($url);
 616  
 617      $extension = resourcelib_get_extension($url->out(false));
 618  
 619      $mediarenderer = $PAGE->get_renderer('core', 'media');
 620      $embedoptions = array(
 621          core_media::OPTION_TRUSTED => true,
 622          core_media::OPTION_BLOCK => true
 623      );
 624  
 625      // find the correct type and print it out
 626      if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) {  // It's an image
 627          $code = resourcelib_embed_image($url, $title);
 628  
 629      } else if ($mediarenderer->can_embed_url($url, $embedoptions)) {
 630          // Media (audio/video) file.
 631          $code = $mediarenderer->embed_url($url, $title, 0, 0, $embedoptions);
 632  
 633      } else {
 634          // anything else - just try object tag enlarged as much as possible
 635          $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype);
 636      }
 637  
 638      return $code;
 639  }
 640  
 641  /**
 642   * Logic to happen when a/some group(s) has/have been deleted in a course.
 643   *
 644   * @param int $courseid The course ID.
 645   * @param int $groupid The group id if it is known
 646   * @return void
 647   */
 648  function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
 649      global $DB;
 650  
 651      $params = array('courseid' => $courseid);
 652      if ($groupid) {
 653          $params['groupid'] = $groupid;
 654          // We just update the group that was deleted.
 655          $sql = "SELECT o.id, o.lessonid
 656                    FROM {lesson_overrides} o
 657                    JOIN {lesson} lesson ON lesson.id = o.lessonid
 658                   WHERE lesson.course = :courseid
 659                     AND o.groupid = :groupid";
 660      } else {
 661          // No groupid, we update all orphaned group overrides for all lessons in course.
 662          $sql = "SELECT o.id, o.lessonid
 663                    FROM {lesson_overrides} o
 664                    JOIN {lesson} lesson ON lesson.id = o.lessonid
 665               LEFT JOIN {groups} grp ON grp.id = o.groupid
 666                   WHERE lesson.course = :courseid
 667                     AND o.groupid IS NOT NULL
 668                     AND grp.id IS NULL";
 669      }
 670      $records = $DB->get_records_sql_menu($sql, $params);
 671      if (!$records) {
 672          return; // Nothing to do.
 673      }
 674      $DB->delete_records_list('lesson_overrides', 'id', array_keys($records));
 675  }
 676  
 677  /**
 678   * Abstract class that page type's MUST inherit from.
 679   *
 680   * This is the abstract class that ALL add page type forms must extend.
 681   * You will notice that all but two of the methods this class contains are final.
 682   * Essentially the only thing that extending classes can do is extend custom_definition.
 683   * OR if it has a special requirement on creation it can extend construction_override
 684   *
 685   * @abstract
 686   * @copyright  2009 Sam Hemelryk
 687   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 688   */
 689  abstract class lesson_add_page_form_base extends moodleform {
 690  
 691      /**
 692       * This is the classic define that is used to identify this pagetype.
 693       * Will be one of LESSON_*
 694       * @var int
 695       */
 696      public $qtype;
 697  
 698      /**
 699       * The simple string that describes the page type e.g. truefalse, multichoice
 700       * @var string
 701       */
 702      public $qtypestring;
 703  
 704      /**
 705       * An array of options used in the htmleditor
 706       * @var array
 707       */
 708      protected $editoroptions = array();
 709  
 710      /**
 711       * True if this is a standard page of false if it does something special.
 712       * Questions are standard pages, branch tables are not
 713       * @var bool
 714       */
 715      protected $standard = true;
 716  
 717      /**
 718       * Answer format supported by question type.
 719       */
 720      protected $answerformat = '';
 721  
 722      /**
 723       * Response format supported by question type.
 724       */
 725      protected $responseformat = '';
 726  
 727      /**
 728       * Each page type can and should override this to add any custom elements to
 729       * the basic form that they want
 730       */
 731      public function custom_definition() {}
 732  
 733      /**
 734       * Returns answer format used by question type.
 735       */
 736      public function get_answer_format() {
 737          return $this->answerformat;
 738      }
 739  
 740      /**
 741       * Returns response format used by question type.
 742       */
 743      public function get_response_format() {
 744          return $this->responseformat;
 745      }
 746  
 747      /**
 748       * Used to determine if this is a standard page or a special page
 749       * @return bool
 750       */
 751      public final function is_standard() {
 752          return (bool)$this->standard;
 753      }
 754  
 755      /**
 756       * Add the required basic elements to the form.
 757       *
 758       * This method adds the basic elements to the form including title and contents
 759       * and then calls custom_definition();
 760       */
 761      public final function definition() {
 762          $mform = $this->_form;
 763          $editoroptions = $this->_customdata['editoroptions'];
 764  
 765          $mform->addElement('header', 'qtypeheading', get_string('createaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson')));
 766  
 767          if (!empty($this->_customdata['returnto'])) {
 768              $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
 769              $mform->setType('returnto', PARAM_URL);
 770          }
 771  
 772          $mform->addElement('hidden', 'id');
 773          $mform->setType('id', PARAM_INT);
 774  
 775          $mform->addElement('hidden', 'pageid');
 776          $mform->setType('pageid', PARAM_INT);
 777  
 778          if ($this->standard === true) {
 779              $mform->addElement('hidden', 'qtype');
 780              $mform->setType('qtype', PARAM_INT);
 781  
 782              $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), array('size'=>70));
 783              $mform->setType('title', PARAM_TEXT);
 784              $mform->addRule('title', get_string('required'), 'required', null, 'client');
 785  
 786              $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
 787              $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions);
 788              $mform->setType('contents_editor', PARAM_RAW);
 789              $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client');
 790          }
 791  
 792          $this->custom_definition();
 793  
 794          if ($this->_customdata['edit'] === true) {
 795              $mform->addElement('hidden', 'edit', 1);
 796              $mform->setType('edit', PARAM_BOOL);
 797              $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
 798          } else if ($this->qtype === 'questiontype') {
 799              $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson'));
 800          } else {
 801              $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
 802          }
 803      }
 804  
 805      /**
 806       * Convenience function: Adds a jumpto select element
 807       *
 808       * @param string $name
 809       * @param string|null $label
 810       * @param int $selected The page to select by default
 811       */
 812      protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
 813          $title = get_string("jump", "lesson");
 814          if ($label === null) {
 815              $label = $title;
 816          }
 817          if (is_int($name)) {
 818              $name = "jumpto[$name]";
 819          }
 820          $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
 821          $this->_form->setDefault($name, $selected);
 822          $this->_form->addHelpButton($name, 'jumps', 'lesson');
 823      }
 824  
 825      /**
 826       * Convenience function: Adds a score input element
 827       *
 828       * @param string $name
 829       * @param string|null $label
 830       * @param mixed $value The default value
 831       */
 832      protected final function add_score($name, $label=null, $value=null) {
 833          if ($label === null) {
 834              $label = get_string("score", "lesson");
 835          }
 836  
 837          if (is_int($name)) {
 838              $name = "score[$name]";
 839          }
 840          $this->_form->addElement('text', $name, $label, array('size'=>5));
 841          $this->_form->setType($name, PARAM_INT);
 842          if ($value !== null) {
 843              $this->_form->setDefault($name, $value);
 844          }
 845          $this->_form->addHelpButton($name, 'score', 'lesson');
 846  
 847          // Score is only used for custom scoring. Disable the element when not in use to stop some confusion.
 848          if (!$this->_customdata['lesson']->custom) {
 849              $this->_form->freeze($name);
 850          }
 851      }
 852  
 853      /**
 854       * Convenience function: Adds an answer editor
 855       *
 856       * @param int $count The count of the element to add
 857       * @param string $label, null means default
 858       * @param bool $required
 859       * @param string $format
 860       * @return void
 861       */
 862      protected final function add_answer($count, $label = null, $required = false, $format= '') {
 863          if ($label === null) {
 864              $label = get_string('answer', 'lesson');
 865          }
 866  
 867          if ($format == LESSON_ANSWER_HTML) {
 868              $this->_form->addElement('editor', 'answer_editor['.$count.']', $label,
 869                      array('rows' => '4', 'columns' => '80'),
 870                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
 871              $this->_form->setType('answer_editor['.$count.']', PARAM_RAW);
 872              $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
 873          } else {
 874              $this->_form->addElement('text', 'answer_editor['.$count.']', $label,
 875                      array('size' => '50', 'maxlength' => '200'));
 876              $this->_form->setType('answer_editor['.$count.']', PARAM_TEXT);
 877          }
 878  
 879          if ($required) {
 880              $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client');
 881          }
 882      }
 883      /**
 884       * Convenience function: Adds an response editor
 885       *
 886       * @param int $count The count of the element to add
 887       * @param string $label, null means default
 888       * @param bool $required
 889       * @return void
 890       */
 891      protected final function add_response($count, $label = null, $required = false) {
 892          if ($label === null) {
 893              $label = get_string('response', 'lesson');
 894          }
 895          $this->_form->addElement('editor', 'response_editor['.$count.']', $label,
 896                   array('rows' => '4', 'columns' => '80'),
 897                   array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
 898          $this->_form->setType('response_editor['.$count.']', PARAM_RAW);
 899          $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
 900  
 901          if ($required) {
 902              $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
 903          }
 904      }
 905  
 906      /**
 907       * A function that gets called upon init of this object by the calling script.
 908       *
 909       * This can be used to process an immediate action if required. Currently it
 910       * is only used in special cases by non-standard page types.
 911       *
 912       * @return bool
 913       */
 914      public function construction_override($pageid, lesson $lesson) {
 915          return true;
 916      }
 917  }
 918  
 919  
 920  
 921  /**
 922   * Class representation of a lesson
 923   *
 924   * This class is used the interact with, and manage a lesson once instantiated.
 925   * If you need to fetch a lesson object you can do so by calling
 926   *
 927   * <code>
 928   * lesson::load($lessonid);
 929   * // or
 930   * $lessonrecord = $DB->get_record('lesson', $lessonid);
 931   * $lesson = new lesson($lessonrecord);
 932   * </code>
 933   *
 934   * The class itself extends lesson_base as all classes within the lesson module should
 935   *
 936   * These properties are from the database
 937   * @property int $id The id of this lesson
 938   * @property int $course The ID of the course this lesson belongs to
 939   * @property string $name The name of this lesson
 940   * @property int $practice Flag to toggle this as a practice lesson
 941   * @property int $modattempts Toggle to allow the user to go back and review answers
 942   * @property int $usepassword Toggle the use of a password for entry
 943   * @property string $password The password to require users to enter
 944   * @property int $dependency ID of another lesson this lesson is dependent on
 945   * @property string $conditions Conditions of the lesson dependency
 946   * @property int $grade The maximum grade a user can achieve (%)
 947   * @property int $custom Toggle custom scoring on or off
 948   * @property int $ongoing Toggle display of an ongoing score
 949   * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
 950   * @property int $maxanswers The max number of answers or branches
 951   * @property int $maxattempts The maximum number of attempts a user can record
 952   * @property int $review Toggle use or wrong answer review button
 953   * @property int $nextpagedefault Override the default next page
 954   * @property int $feedback Toggles display of default feedback
 955   * @property int $minquestions Sets a minimum value of pages seen when calculating grades
 956   * @property int $maxpages Maximum number of pages this lesson can contain
 957   * @property int $retake Flag to allow users to retake a lesson
 958   * @property int $activitylink Relate this lesson to another lesson
 959   * @property string $mediafile File to pop up to or webpage to display
 960   * @property int $mediaheight Sets the height of the media file popup
 961   * @property int $mediawidth Sets the width of the media file popup
 962   * @property int $mediaclose Toggle display of a media close button
 963   * @property int $slideshow Flag for whether branch pages should be shown as slideshows
 964   * @property int $width Width of slideshow
 965   * @property int $height Height of slideshow
 966   * @property string $bgcolor Background colour of slideshow
 967   * @property int $displayleft Display a left menu
 968   * @property int $displayleftif Sets the condition on which the left menu is displayed
 969   * @property int $progressbar Flag to toggle display of a lesson progress bar
 970   * @property int $available Timestamp of when this lesson becomes available
 971   * @property int $deadline Timestamp of when this lesson is no longer available
 972   * @property int $timemodified Timestamp when lesson was last modified
 973   *
 974   * These properties are calculated
 975   * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
 976   * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
 977   *
 978   * @copyright  2009 Sam Hemelryk
 979   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 980   */
 981  class lesson extends lesson_base {
 982  
 983      /**
 984       * The id of the first page (where prevpageid = 0) gets set and retrieved by
 985       * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
 986       * @var int
 987       */
 988      protected $firstpageid = null;
 989      /**
 990       * The id of the last page (where nextpageid = 0) gets set and retrieved by
 991       * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
 992       * @var int
 993       */
 994      protected $lastpageid = null;
 995      /**
 996       * An array used to cache the pages associated with this lesson after the first
 997       * time they have been loaded.
 998       * A note to developers: If you are going to be working with MORE than one or
 999       * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
1000       * in order to save excess database queries.
1001       * @var array An array of lesson_page objects
1002       */
1003      protected $pages = array();
1004      /**
1005       * Flag that gets set to true once all of the pages associated with the lesson
1006       * have been loaded.
1007       * @var bool
1008       */
1009      protected $loadedallpages = false;
1010  
1011      /**
1012       * Simply generates a lesson object given an array/object of properties
1013       * Overrides {@see lesson_base->create()}
1014       * @static
1015       * @param object|array $properties
1016       * @return lesson
1017       */
1018      public static function create($properties) {
1019          return new lesson($properties);
1020      }
1021  
1022      /**
1023       * Generates a lesson object from the database given its id
1024       * @static
1025       * @param int $lessonid
1026       * @return lesson
1027       */
1028      public static function load($lessonid) {
1029          global $DB;
1030  
1031          if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
1032              print_error('invalidcoursemodule');
1033          }
1034          return new lesson($lesson);
1035      }
1036  
1037      /**
1038       * Deletes this lesson from the database
1039       */
1040      public function delete() {
1041          global $CFG, $DB;
1042          require_once($CFG->libdir.'/gradelib.php');
1043          require_once($CFG->dirroot.'/calendar/lib.php');
1044  
1045          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1046          $context = context_module::instance($cm->id);
1047  
1048          $this->delete_all_overrides();
1049  
1050          $DB->delete_records("lesson", array("id"=>$this->properties->id));
1051          $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
1052          $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
1053          $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
1054          $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
1055          $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
1056          $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
1057          if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
1058              foreach($events as $event) {
1059                  $event = calendar_event::load($event);
1060                  $event->delete();
1061              }
1062          }
1063  
1064          // Delete files associated with this module.
1065          $fs = get_file_storage();
1066          $fs->delete_area_files($context->id);
1067  
1068          grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1));
1069          return true;
1070      }
1071  
1072      /**
1073       * Deletes a lesson override from the database and clears any corresponding calendar events
1074       *
1075       * @param int $overrideid The id of the override being deleted
1076       * @return bool true on success
1077       */
1078      public function delete_override($overrideid) {
1079          global $CFG, $DB;
1080  
1081          require_once($CFG->dirroot . '/calendar/lib.php');
1082  
1083          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1084  
1085          $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST);
1086  
1087          // Delete the events.
1088          $conds = array('modulename' => 'lesson',
1089                  'instance' => $this->properties->id);
1090          if (isset($override->userid)) {
1091              $conds['userid'] = $override->userid;
1092          } else {
1093              $conds['groupid'] = $override->groupid;
1094          }
1095          $events = $DB->get_records('event', $conds);
1096          foreach ($events as $event) {
1097              $eventold = calendar_event::load($event);
1098              $eventold->delete();
1099          }
1100  
1101          $DB->delete_records('lesson_overrides', array('id' => $overrideid));
1102  
1103          // Set the common parameters for one of the events we will be triggering.
1104          $params = array(
1105              'objectid' => $override->id,
1106              'context' => context_module::instance($cm->id),
1107              'other' => array(
1108                  'lessonid' => $override->lessonid
1109              )
1110          );
1111          // Determine which override deleted event to fire.
1112          if (!empty($override->userid)) {
1113              $params['relateduserid'] = $override->userid;
1114              $event = \mod_lesson\event\user_override_deleted::create($params);
1115          } else {
1116              $params['other']['groupid'] = $override->groupid;
1117              $event = \mod_lesson\event\group_override_deleted::create($params);
1118          }
1119  
1120          // Trigger the override deleted event.
1121          $event->add_record_snapshot('lesson_overrides', $override);
1122          $event->trigger();
1123  
1124          return true;
1125      }
1126  
1127      /**
1128       * Deletes all lesson overrides from the database and clears any corresponding calendar events
1129       */
1130      public function delete_all_overrides() {
1131          global $DB;
1132  
1133          $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id');
1134          foreach ($overrides as $override) {
1135              $this->delete_override($override->id);
1136          }
1137      }
1138  
1139      /**
1140       * Updates the lesson properties with override information for a user.
1141       *
1142       * Algorithm:  For each lesson setting, if there is a matching user-specific override,
1143       *   then use that otherwise, if there are group-specific overrides, return the most
1144       *   lenient combination of them.  If neither applies, leave the quiz setting unchanged.
1145       *
1146       *   Special case: if there is more than one password that applies to the user, then
1147       *   lesson->extrapasswords will contain an array of strings giving the remaining
1148       *   passwords.
1149       *
1150       * @param int $userid The userid.
1151       */
1152      public function update_effective_access($userid) {
1153          global $DB;
1154  
1155          // Check for user override.
1156          $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid));
1157  
1158          if (!$override) {
1159              $override = new stdClass();
1160              $override->available = null;
1161              $override->deadline = null;
1162              $override->timelimit = null;
1163              $override->review = null;
1164              $override->maxattempts = null;
1165              $override->retake = null;
1166              $override->password = null;
1167          }
1168  
1169          // Check for group overrides.
1170          $groupings = groups_get_user_groups($this->properties->course, $userid);
1171  
1172          if (!empty($groupings[0])) {
1173              // Select all overrides that apply to the User's groups.
1174              list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1175              $sql = "SELECT * FROM {lesson_overrides}
1176                      WHERE groupid $extra AND lessonid = ?";
1177              $params[] = $this->properties->id;
1178              $records = $DB->get_records_sql($sql, $params);
1179  
1180              // Combine the overrides.
1181              $availables = array();
1182              $deadlines = array();
1183              $timelimits = array();
1184              $reviews = array();
1185              $attempts = array();
1186              $retakes = array();
1187              $passwords = array();
1188  
1189              foreach ($records as $gpoverride) {
1190                  if (isset($gpoverride->available)) {
1191                      $availables[] = $gpoverride->available;
1192                  }
1193                  if (isset($gpoverride->deadline)) {
1194                      $deadlines[] = $gpoverride->deadline;
1195                  }
1196                  if (isset($gpoverride->timelimit)) {
1197                      $timelimits[] = $gpoverride->timelimit;
1198                  }
1199                  if (isset($gpoverride->review)) {
1200                      $reviews[] = $gpoverride->review;
1201                  }
1202                  if (isset($gpoverride->maxattempts)) {
1203                      $attempts[] = $gpoverride->maxattempts;
1204                  }
1205                  if (isset($gpoverride->retake)) {
1206                      $retakes[] = $gpoverride->retake;
1207                  }
1208                  if (isset($gpoverride->password)) {
1209                      $passwords[] = $gpoverride->password;
1210                  }
1211              }
1212              // If there is a user override for a setting, ignore the group override.
1213              if (is_null($override->available) && count($availables)) {
1214                  $override->available = min($availables);
1215              }
1216              if (is_null($override->deadline) && count($deadlines)) {
1217                  if (in_array(0, $deadlines)) {
1218                      $override->deadline = 0;
1219                  } else {
1220                      $override->deadline = max($deadlines);
1221                  }
1222              }
1223              if (is_null($override->timelimit) && count($timelimits)) {
1224                  if (in_array(0, $timelimits)) {
1225                      $override->timelimit = 0;
1226                  } else {
1227                      $override->timelimit = max($timelimits);
1228                  }
1229              }
1230              if (is_null($override->review) && count($reviews)) {
1231                  $override->review = max($reviews);
1232              }
1233              if (is_null($override->maxattempts) && count($attempts)) {
1234                  $override->maxattempts = max($attempts);
1235              }
1236              if (is_null($override->retake) && count($retakes)) {
1237                  $override->retake = max($retakes);
1238              }
1239              if (is_null($override->password) && count($passwords)) {
1240                  $override->password = array_shift($passwords);
1241                  if (count($passwords)) {
1242                      $override->extrapasswords = $passwords;
1243                  }
1244              }
1245  
1246          }
1247  
1248          // Merge with lesson defaults.
1249          $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake');
1250          foreach ($keys as $key) {
1251              if (isset($override->{$key})) {
1252                  $this->properties->{$key} = $override->{$key};
1253              }
1254          }
1255  
1256          // Special handling of lesson usepassword and password.
1257          if (isset($override->password)) {
1258              if ($override->password == '') {
1259                  $this->properties->usepassword = 0;
1260              } else {
1261                  $this->properties->usepassword = 1;
1262                  $this->properties->password = $override->password;
1263                  if (isset($override->extrapasswords)) {
1264                      $this->properties->extrapasswords = $override->extrapasswords;
1265                  }
1266              }
1267          }
1268      }
1269  
1270      /**
1271       * Fetches messages from the session that may have been set in previous page
1272       * actions.
1273       *
1274       * <code>
1275       * // Do not call this method directly instead use
1276       * $lesson->messages;
1277       * </code>
1278       *
1279       * @return array
1280       */
1281      protected function get_messages() {
1282          global $SESSION;
1283  
1284          $messages = array();
1285          if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1286              $messages = $SESSION->lesson_messages[$this->properties->id];
1287              unset($SESSION->lesson_messages[$this->properties->id]);
1288          }
1289  
1290          return $messages;
1291      }
1292  
1293      /**
1294       * Get all of the attempts for the current user.
1295       *
1296       * @param int $retries
1297       * @param bool $correct Optional: only fetch correct attempts
1298       * @param int $pageid Optional: only fetch attempts at the given page
1299       * @param int $userid Optional: defaults to the current user if not set
1300       * @return array|false
1301       */
1302      public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1303          global $USER, $DB;
1304          $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1305          if ($correct) {
1306              $params['correct'] = 1;
1307          }
1308          if ($pageid !== null) {
1309              $params['pageid'] = $pageid;
1310          }
1311          if ($userid === null) {
1312              $params['userid'] = $USER->id;
1313          }
1314          return $DB->get_records('lesson_attempts', $params, 'timeseen ASC');
1315      }
1316  
1317      /**
1318       * Returns the first page for the lesson or false if there isn't one.
1319       *
1320       * This method should be called via the magic method __get();
1321       * <code>
1322       * $firstpage = $lesson->firstpage;
1323       * </code>
1324       *
1325       * @return lesson_page|bool Returns the lesson_page specialised object or false
1326       */
1327      protected function get_firstpage() {
1328          $pages = $this->load_all_pages();
1329          if (count($pages) > 0) {
1330              foreach ($pages as $page) {
1331                  if ((int)$page->prevpageid === 0) {
1332                      return $page;
1333                  }
1334              }
1335          }
1336          return false;
1337      }
1338  
1339      /**
1340       * Returns the last page for the lesson or false if there isn't one.
1341       *
1342       * This method should be called via the magic method __get();
1343       * <code>
1344       * $lastpage = $lesson->lastpage;
1345       * </code>
1346       *
1347       * @return lesson_page|bool Returns the lesson_page specialised object or false
1348       */
1349      protected function get_lastpage() {
1350          $pages = $this->load_all_pages();
1351          if (count($pages) > 0) {
1352              foreach ($pages as $page) {
1353                  if ((int)$page->nextpageid === 0) {
1354                      return $page;
1355                  }
1356              }
1357          }
1358          return false;
1359      }
1360  
1361      /**
1362       * Returns the id of the first page of this lesson. (prevpageid = 0)
1363       * @return int
1364       */
1365      protected function get_firstpageid() {
1366          global $DB;
1367          if ($this->firstpageid == null) {
1368              if (!$this->loadedallpages) {
1369                  $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
1370                  if (!$firstpageid) {
1371                      print_error('cannotfindfirstpage', 'lesson');
1372                  }
1373                  $this->firstpageid = $firstpageid;
1374              } else {
1375                  $firstpage = $this->get_firstpage();
1376                  $this->firstpageid = $firstpage->id;
1377              }
1378          }
1379          return $this->firstpageid;
1380      }
1381  
1382      /**
1383       * Returns the id of the last page of this lesson. (nextpageid = 0)
1384       * @return int
1385       */
1386      public function get_lastpageid() {
1387          global $DB;
1388          if ($this->lastpageid == null) {
1389              if (!$this->loadedallpages) {
1390                  $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
1391                  if (!$lastpageid) {
1392                      print_error('cannotfindlastpage', 'lesson');
1393                  }
1394                  $this->lastpageid = $lastpageid;
1395              } else {
1396                  $lastpageid = $this->get_lastpage();
1397                  $this->lastpageid = $lastpageid->id;
1398              }
1399          }
1400  
1401          return $this->lastpageid;
1402      }
1403  
1404       /**
1405       * Gets the next page id to display after the one that is provided.
1406       * @param int $nextpageid
1407       * @return bool
1408       */
1409      public function get_next_page($nextpageid) {
1410          global $USER, $DB;
1411          $allpages = $this->load_all_pages();
1412          if ($this->properties->nextpagedefault) {
1413              // in Flash Card mode...first get number of retakes
1414              $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
1415              shuffle($allpages);
1416              $found = false;
1417              if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
1418                  foreach ($allpages as $nextpage) {
1419                      if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) {
1420                          $found = true;
1421                          break;
1422                      }
1423                  }
1424              } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
1425                  foreach ($allpages as $nextpage) {
1426                      if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) {
1427                          $found = true;
1428                          break;
1429                      }
1430                  }
1431              }
1432              if ($found) {
1433                  if ($this->properties->maxpages) {
1434                      // check number of pages viewed (in the lesson)
1435                      if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) {
1436                          return LESSON_EOL;
1437                      }
1438                  }
1439                  return $nextpage->id;
1440              }
1441          }
1442          // In a normal lesson mode
1443          foreach ($allpages as $nextpage) {
1444              if ((int)$nextpage->id === (int)$nextpageid) {
1445                  return $nextpage->id;
1446              }
1447          }
1448          return LESSON_EOL;
1449      }
1450  
1451      /**
1452       * Sets a message against the session for this lesson that will displayed next
1453       * time the lesson processes messages
1454       *
1455       * @param string $message
1456       * @param string $class
1457       * @param string $align
1458       * @return bool
1459       */
1460      public function add_message($message, $class="notifyproblem", $align='center') {
1461          global $SESSION;
1462  
1463          if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
1464              $SESSION->lesson_messages = array();
1465              $SESSION->lesson_messages[$this->properties->id] = array();
1466          } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1467              $SESSION->lesson_messages[$this->properties->id] = array();
1468          }
1469  
1470          $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
1471  
1472          return true;
1473      }
1474  
1475      /**
1476       * Check if the lesson is accessible at the present time
1477       * @return bool True if the lesson is accessible, false otherwise
1478       */
1479      public function is_accessible() {
1480          $available = $this->properties->available;
1481          $deadline = $this->properties->deadline;
1482          return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
1483      }
1484  
1485      /**
1486       * Starts the lesson time for the current user
1487       * @return bool Returns true
1488       */
1489      public function start_timer() {
1490          global $USER, $DB;
1491  
1492          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
1493              false, MUST_EXIST);
1494  
1495          // Trigger lesson started event.
1496          $event = \mod_lesson\event\lesson_started::create(array(
1497              'objectid' => $this->properties()->id,
1498              'context' => context_module::instance($cm->id),
1499              'courseid' => $this->properties()->course
1500          ));
1501          $event->trigger();
1502  
1503          $USER->startlesson[$this->properties->id] = true;
1504          $startlesson = new stdClass;
1505          $startlesson->lessonid = $this->properties->id;
1506          $startlesson->userid = $USER->id;
1507          $startlesson->starttime = time();
1508          $startlesson->lessontime = time();
1509          $DB->insert_record('lesson_timer', $startlesson);
1510          if ($this->properties->timelimit) {
1511              $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center');
1512          }
1513          return true;
1514      }
1515  
1516      /**
1517       * Updates the timer to the current time and returns the new timer object
1518       * @param bool $restart If set to true the timer is restarted
1519       * @param bool $continue If set to true AND $restart=true then the timer
1520       *                        will continue from a previous attempt
1521       * @return stdClass The new timer
1522       */
1523      public function update_timer($restart=false, $continue=false, $endreached =false) {
1524          global $USER, $DB;
1525  
1526          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1527  
1528          // clock code
1529          // get time information for this user
1530          $params = array("lessonid" => $this->properties->id, "userid" => $USER->id);
1531          if (!$timer = $DB->get_records('lesson_timer', $params, 'starttime DESC', '*', 0, 1)) {
1532              $this->start_timer();
1533              $timer = $DB->get_records('lesson_timer', $params, 'starttime DESC', '*', 0, 1);
1534          }
1535          $timer = current($timer); // This will get the latest start time record.
1536  
1537          if ($restart) {
1538              if ($continue) {
1539                  // continue a previous test, need to update the clock  (think this option is disabled atm)
1540                  $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
1541  
1542                  // Trigger lesson resumed event.
1543                  $event = \mod_lesson\event\lesson_resumed::create(array(
1544                      'objectid' => $this->properties->id,
1545                      'context' => context_module::instance($cm->id),
1546                      'courseid' => $this->properties->course
1547                  ));
1548                  $event->trigger();
1549  
1550              } else {
1551                  // starting over, so reset the clock
1552                  $timer->starttime = time();
1553  
1554                  // Trigger lesson restarted event.
1555                  $event = \mod_lesson\event\lesson_restarted::create(array(
1556                      'objectid' => $this->properties->id,
1557                      'context' => context_module::instance($cm->id),
1558                      'courseid' => $this->properties->course
1559                  ));
1560                  $event->trigger();
1561  
1562              }
1563          }
1564  
1565          $timer->lessontime = time();
1566          $timer->completed = $endreached;
1567          $DB->update_record('lesson_timer', $timer);
1568  
1569          // Update completion state.
1570          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
1571              false, MUST_EXIST);
1572          $course = get_course($cm->course);
1573          $completion = new completion_info($course);
1574          if ($completion->is_enabled($cm) && $this->properties()->completiontimespent > 0) {
1575              $completion->update_state($cm, COMPLETION_COMPLETE);
1576          }
1577          return $timer;
1578      }
1579  
1580      /**
1581       * Updates the timer to the current time then stops it by unsetting the user var
1582       * @return bool Returns true
1583       */
1584      public function stop_timer() {
1585          global $USER, $DB;
1586          unset($USER->startlesson[$this->properties->id]);
1587  
1588          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
1589              false, MUST_EXIST);
1590  
1591          // Trigger lesson ended event.
1592          $event = \mod_lesson\event\lesson_ended::create(array(
1593              'objectid' => $this->properties()->id,
1594              'context' => context_module::instance($cm->id),
1595              'courseid' => $this->properties()->course
1596          ));
1597          $event->trigger();
1598  
1599          return $this->update_timer(false, false, true);
1600      }
1601  
1602      /**
1603       * Checks to see if the lesson has pages
1604       */
1605      public function has_pages() {
1606          global $DB;
1607          $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
1608          return ($pagecount>0);
1609      }
1610  
1611      /**
1612       * Returns the link for the related activity
1613       * @return array|false
1614       */
1615      public function link_for_activitylink() {
1616          global $DB;
1617          $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
1618          if ($module) {
1619              $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
1620              if ($modname) {
1621                  $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
1622                  if ($instancename) {
1623                      return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php', array('id'=>$this->properties->activitylink)),
1624                          get_string('activitylinkname', 'lesson', $instancename),
1625                          array('class'=>'centerpadded lessonbutton standardbutton'));
1626                  }
1627              }
1628          }
1629          return '';
1630      }
1631  
1632      /**
1633       * Loads the requested page.
1634       *
1635       * This function will return the requested page id as either a specialised
1636       * lesson_page object OR as a generic lesson_page.
1637       * If the page has been loaded previously it will be returned from the pages
1638       * array, otherwise it will be loaded from the database first
1639       *
1640       * @param int $pageid
1641       * @return lesson_page A lesson_page object or an object that extends it
1642       */
1643      public function load_page($pageid) {
1644          if (!array_key_exists($pageid, $this->pages)) {
1645              $manager = lesson_page_type_manager::get($this);
1646              $this->pages[$pageid] = $manager->load_page($pageid, $this);
1647          }
1648          return $this->pages[$pageid];
1649      }
1650  
1651      /**
1652       * Loads ALL of the pages for this lesson
1653       *
1654       * @return array An array containing all pages from this lesson
1655       */
1656      public function load_all_pages() {
1657          if (!$this->loadedallpages) {
1658              $manager = lesson_page_type_manager::get($this);
1659              $this->pages = $manager->load_all_pages($this);
1660              $this->loadedallpages = true;
1661          }
1662          return $this->pages;
1663      }
1664  
1665      /**
1666       * Determines if a jumpto value is correct or not.
1667       *
1668       * returns true if jumpto page is (logically) after the pageid page or
1669       * if the jumpto value is a special value.  Returns false in all other cases.
1670       *
1671       * @param int $pageid Id of the page from which you are jumping from.
1672       * @param int $jumpto The jumpto number.
1673       * @return boolean True or false after a series of tests.
1674       **/
1675      public function jumpto_is_correct($pageid, $jumpto) {
1676          global $DB;
1677  
1678          // first test the special values
1679          if (!$jumpto) {
1680              // same page
1681              return false;
1682          } elseif ($jumpto == LESSON_NEXTPAGE) {
1683              return true;
1684          } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
1685              return true;
1686          } elseif ($jumpto == LESSON_RANDOMPAGE) {
1687              return true;
1688          } elseif ($jumpto == LESSON_CLUSTERJUMP) {
1689              return true;
1690          } elseif ($jumpto == LESSON_EOL) {
1691              return true;
1692          }
1693  
1694          $pages = $this->load_all_pages();
1695          $apageid = $pages[$pageid]->nextpageid;
1696          while ($apageid != 0) {
1697              if ($jumpto == $apageid) {
1698                  return true;
1699              }
1700              $apageid = $pages[$apageid]->nextpageid;
1701          }
1702          return false;
1703      }
1704  
1705      /**
1706       * Returns the time a user has remaining on this lesson
1707       * @param int $starttime Starttime timestamp
1708       * @return string
1709       */
1710      public function time_remaining($starttime) {
1711          $timeleft = $starttime + $this->properties->timelimit - time();
1712          $hours = floor($timeleft/3600);
1713          $timeleft = $timeleft - ($hours * 3600);
1714          $minutes = floor($timeleft/60);
1715          $secs = $timeleft - ($minutes * 60);
1716  
1717          if ($minutes < 10) {
1718              $minutes = "0$minutes";
1719          }
1720          if ($secs < 10) {
1721              $secs = "0$secs";
1722          }
1723          $output   = array();
1724          $output[] = $hours;
1725          $output[] = $minutes;
1726          $output[] = $secs;
1727          $output = implode(':', $output);
1728          return $output;
1729      }
1730  
1731      /**
1732       * Interprets LESSON_CLUSTERJUMP jumpto value.
1733       *
1734       * This will select a page randomly
1735       * and the page selected will be inbetween a cluster page and end of clutter or end of lesson
1736       * and the page selected will be a page that has not been viewed already
1737       * and if any pages are within a branch table or end of branch then only 1 page within
1738       * the branch table or end of branch will be randomly selected (sub clustering).
1739       *
1740       * @param int $pageid Id of the current page from which we are jumping from.
1741       * @param int $userid Id of the user.
1742       * @return int The id of the next page.
1743       **/
1744      public function cluster_jump($pageid, $userid=null) {
1745          global $DB, $USER;
1746  
1747          if ($userid===null) {
1748              $userid = $USER->id;
1749          }
1750          // get the number of retakes
1751          if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
1752              $retakes = 0;
1753          }
1754          // get all the lesson_attempts aka what the user has seen
1755          $seenpages = array();
1756          if ($attempts = $this->get_attempts($retakes)) {
1757              foreach ($attempts as $attempt) {
1758                  $seenpages[$attempt->pageid] = $attempt->pageid;
1759              }
1760  
1761          }
1762  
1763          // get the lesson pages
1764          $lessonpages = $this->load_all_pages();
1765          // find the start of the cluster
1766          while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
1767              if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
1768                  break;
1769              }
1770              $pageid = $lessonpages[$pageid]->prevpageid;
1771          }
1772  
1773          $clusterpages = array();
1774          $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
1775          $unseen = array();
1776          foreach ($clusterpages as $key=>$cluster) {
1777              // Remove the page if  it is in a branch table or is an endofbranch.
1778              if ($this->is_sub_page_of_type($cluster->id,
1779                      array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))
1780                      || $cluster->qtype == LESSON_PAGE_ENDOFBRANCH) {
1781                  unset($clusterpages[$key]);
1782              } else if ($cluster->qtype == LESSON_PAGE_BRANCHTABLE) {
1783                  // If branchtable, check to see if any pages inside have been viewed.
1784                  $branchpages = $this->get_sub_pages_of($cluster->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
1785                  $flag = true;
1786                  foreach ($branchpages as $branchpage) {
1787                      if (array_key_exists($branchpage->id, $seenpages)) {  // Check if any of the pages have been viewed.
1788                          $flag = false;
1789                      }
1790                  }
1791                  if ($flag && count($branchpages) > 0) {
1792                      // Add branch table.
1793                      $unseen[] = $cluster;
1794                  }
1795              } elseif ($cluster->is_unseen($seenpages)) {
1796                  $unseen[] = $cluster;
1797              }
1798          }
1799  
1800          if (count($unseen) > 0) {
1801              // it does not contain elements, then use exitjump, otherwise find out next page/branch
1802              $nextpage = $unseen[rand(0, count($unseen)-1)];
1803              if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
1804                  // if branch table, then pick a random page inside of it
1805                  $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
1806                  return $branchpages[rand(0, count($branchpages)-1)]->id;
1807              } else { // otherwise, return the page's id
1808                  return $nextpage->id;
1809              }
1810          } else {
1811              // seen all there is to see, leave the cluster
1812              if (end($clusterpages)->nextpageid == 0) {
1813                  return LESSON_EOL;
1814              } else {
1815                  $clusterendid = $pageid;
1816                  while ($clusterendid != 0) { // This condition should not be satisfied... should be an end of cluster page.
1817                      if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_ENDOFCLUSTER) {
1818                          break;
1819                      }
1820                      $clusterendid = $lessonpages[$clusterendid]->nextpageid;
1821                  }
1822                  $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
1823                  if ($exitjump == LESSON_NEXTPAGE) {
1824                      $exitjump = $lessonpages[$clusterendid]->nextpageid;
1825                  }
1826                  if ($exitjump == 0) {
1827                      return LESSON_EOL;
1828                  } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
1829                      return $exitjump;
1830                  } else {
1831                      if (!array_key_exists($exitjump, $lessonpages)) {
1832                          $found = false;
1833                          foreach ($lessonpages as $page) {
1834                              if ($page->id === $clusterendid) {
1835                                  $found = true;
1836                              } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
1837                                  $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
1838                                  if ($exitjump == LESSON_NEXTPAGE) {
1839                                      $exitjump = $lessonpages[$page->id]->nextpageid;
1840                                  }
1841                                  break;
1842                              }
1843                          }
1844                      }
1845                      if (!array_key_exists($exitjump, $lessonpages)) {
1846                          return LESSON_EOL;
1847                      }
1848                      return $exitjump;
1849                  }
1850              }
1851          }
1852      }
1853  
1854      /**
1855       * Finds all pages that appear to be a subtype of the provided pageid until
1856       * an end point specified within $ends is encountered or no more pages exist
1857       *
1858       * @param int $pageid
1859       * @param array $ends An array of LESSON_PAGE_* types that signify an end of
1860       *               the subtype
1861       * @return array An array of specialised lesson_page objects
1862       */
1863      public function get_sub_pages_of($pageid, array $ends) {
1864          $lessonpages = $this->load_all_pages();
1865          $pageid = $lessonpages[$pageid]->nextpageid;  // move to the first page after the branch table
1866          $pages = array();
1867  
1868          while (true) {
1869              if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
1870                  break;
1871              }
1872              $pages[] = $lessonpages[$pageid];
1873              $pageid = $lessonpages[$pageid]->nextpageid;
1874          }
1875  
1876          return $pages;
1877      }
1878  
1879      /**
1880       * Checks to see if the specified page[id] is a subpage of a type specified in
1881       * the $types array, until either there are no more pages of we find a type
1882       * corresponding to that of a type specified in $ends
1883       *
1884       * @param int $pageid The id of the page to check
1885       * @param array $types An array of types that would signify this page was a subpage
1886       * @param array $ends An array of types that mean this is not a subpage
1887       * @return bool
1888       */
1889      public function is_sub_page_of_type($pageid, array $types, array $ends) {
1890          $pages = $this->load_all_pages();
1891          $pageid = $pages[$pageid]->prevpageid; // move up one
1892  
1893          array_unshift($ends, 0);
1894          // go up the pages till branch table
1895          while (true) {
1896              if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
1897                  return false;
1898              } else if (in_array($pages[$pageid]->qtype, $types)) {
1899                  return true;
1900              }
1901              $pageid = $pages[$pageid]->prevpageid;
1902          }
1903      }
1904  
1905      /**
1906       * Move a page resorting all other pages.
1907       *
1908       * @param int $pageid
1909       * @param int $after
1910       * @return void
1911       */
1912      public function resort_pages($pageid, $after) {
1913          global $CFG;
1914  
1915          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1916          $context = context_module::instance($cm->id);
1917  
1918          $pages = $this->load_all_pages();
1919  
1920          if (!array_key_exists($pageid, $pages) || ($after!=0 && !array_key_exists($after, $pages))) {
1921              print_error('cannotfindpages', 'lesson', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id");
1922          }
1923  
1924          $pagetomove = clone($pages[$pageid]);
1925          unset($pages[$pageid]);
1926  
1927          $pageids = array();
1928          if ($after === 0) {
1929              $pageids['p0'] = $pageid;
1930          }
1931          foreach ($pages as $page) {
1932              $pageids[] = $page->id;
1933              if ($page->id == $after) {
1934                  $pageids[] = $pageid;
1935              }
1936          }
1937  
1938          $pageidsref = $pageids;
1939          reset($pageidsref);
1940          $prev = 0;
1941          $next = next($pageidsref);
1942          foreach ($pageids as $pid) {
1943              if ($pid === $pageid) {
1944                  $page = $pagetomove;
1945              } else {
1946                  $page = $pages[$pid];
1947              }
1948              if ($page->prevpageid != $prev || $page->nextpageid != $next) {
1949                  $page->move($next, $prev);
1950  
1951                  if ($pid === $pageid) {
1952                      // We will trigger an event.
1953                      $pageupdated = array('next' => $next, 'prev' => $prev);
1954                  }
1955              }
1956  
1957              $prev = $page->id;
1958              $next = next($pageidsref);
1959              if (!$next) {
1960                  $next = 0;
1961              }
1962          }
1963  
1964          // Trigger an event: page moved.
1965          if (!empty($pageupdated)) {
1966              $eventparams = array(
1967                  'context' => $context,
1968                  'objectid' => $pageid,
1969                  'other' => array(
1970                      'pagetype' => $page->get_typestring(),
1971                      'prevpageid' => $pageupdated['prev'],
1972                      'nextpageid' => $pageupdated['next']
1973                  )
1974              );
1975              $event = \mod_lesson\event\page_moved::create($eventparams);
1976              $event->trigger();
1977          }
1978  
1979      }
1980  }
1981  
1982  
1983  /**
1984   * Abstract class to provide a core functions to the all lesson classes
1985   *
1986   * This class should be abstracted by ALL classes with the lesson module to ensure
1987   * that all classes within this module can be interacted with in the same way.
1988   *
1989   * This class provides the user with a basic properties array that can be fetched
1990   * or set via magic methods, or alternatively by defining methods get_blah() or
1991   * set_blah() within the extending object.
1992   *
1993   * @copyright  2009 Sam Hemelryk
1994   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1995   */
1996  abstract class lesson_base {
1997  
1998      /**
1999       * An object containing properties
2000       * @var stdClass
2001       */
2002      protected $properties;
2003  
2004      /**
2005       * The constructor
2006       * @param stdClass $properties
2007       */
2008      public function __construct($properties) {
2009          $this->properties = (object)$properties;
2010      }
2011  
2012      /**
2013       * Magic property method
2014       *
2015       * Attempts to call a set_$key method if one exists otherwise falls back
2016       * to simply set the property
2017       *
2018       * @param string $key
2019       * @param mixed $value
2020       */
2021      public function __set($key, $value) {
2022          if (method_exists($this, 'set_'.$key)) {
2023              $this->{'set_'.$key}($value);
2024          }
2025          $this->properties->{$key} = $value;
2026      }
2027  
2028      /**
2029       * Magic get method
2030       *
2031       * Attempts to call a get_$key method to return the property and ralls over
2032       * to return the raw property
2033       *
2034       * @param str $key
2035       * @return mixed
2036       */
2037      public function __get($key) {
2038          if (method_exists($this, 'get_'.$key)) {
2039              return $this->{'get_'.$key}();
2040          }
2041          return $this->properties->{$key};
2042      }
2043  
2044      /**
2045       * Stupid PHP needs an isset magic method if you use the get magic method and
2046       * still want empty calls to work.... blah ~!
2047       *
2048       * @param string $key
2049       * @return bool
2050       */
2051      public function __isset($key) {
2052          if (method_exists($this, 'get_'.$key)) {
2053              $val = $this->{'get_'.$key}();
2054              return !empty($val);
2055          }
2056          return !empty($this->properties->{$key});
2057      }
2058  
2059      //NOTE: E_STRICT does not allow to change function signature!
2060  
2061      /**
2062       * If implemented should create a new instance, save it in the DB and return it
2063       */
2064      //public static function create() {}
2065      /**
2066       * If implemented should load an instance from the DB and return it
2067       */
2068      //public static function load() {}
2069      /**
2070       * Fetches all of the properties of the object
2071       * @return stdClass
2072       */
2073      public function properties() {
2074          return $this->properties;
2075      }
2076  }
2077  
2078  
2079  /**
2080   * Abstract class representation of a page associated with a lesson.
2081   *
2082   * This class should MUST be extended by all specialised page types defined in
2083   * mod/lesson/pagetypes/.
2084   * There are a handful of abstract methods that need to be defined as well as
2085   * severl methods that can optionally be defined in order to make the page type
2086   * operate in the desired way
2087   *
2088   * Database properties
2089   * @property int $id The id of this lesson page
2090   * @property int $lessonid The id of the lesson this page belongs to
2091   * @property int $prevpageid The id of the page before this one
2092   * @property int $nextpageid The id of the next page in the page sequence
2093   * @property int $qtype Identifies the page type of this page
2094   * @property int $qoption Used to record page type specific options
2095   * @property int $layout Used to record page specific layout selections
2096   * @property int $display Used to record page specific display selections
2097   * @property int $timecreated Timestamp for when the page was created
2098   * @property int $timemodified Timestamp for when the page was last modified
2099   * @property string $title The title of this page
2100   * @property string $contents The rich content shown to describe the page
2101   * @property int $contentsformat The format of the contents field
2102   *
2103   * Calculated properties
2104   * @property-read array $answers An array of answers for this page
2105   * @property-read bool $displayinmenublock Toggles display in the left menu block
2106   * @property-read array $jumps An array containing all the jumps this page uses
2107   * @property-read lesson $lesson The lesson this page belongs to
2108   * @property-read int $type The type of the page [question | structure]
2109   * @property-read typeid The unique identifier for the page type
2110   * @property-read typestring The string that describes this page type
2111   *
2112   * @abstract
2113   * @copyright  2009 Sam Hemelryk
2114   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2115   */
2116  abstract class lesson_page extends lesson_base {
2117  
2118      /**
2119       * A reference to the lesson this page belongs to
2120       * @var lesson
2121       */
2122      protected $lesson = null;
2123      /**
2124       * Contains the answers to this lesson_page once loaded
2125       * @var null|array
2126       */
2127      protected $answers = null;
2128      /**
2129       * This sets the type of the page, can be one of the constants defined below
2130       * @var int
2131       */
2132      protected $type = 0;
2133  
2134      /**
2135       * Constants used to identify the type of the page
2136       */
2137      const TYPE_QUESTION = 0;
2138      const TYPE_STRUCTURE = 1;
2139  
2140      /**
2141       * This method should return the integer used to identify the page type within
2142       * the database and throughout code. This maps back to the defines used in 1.x
2143       * @abstract
2144       * @return int
2145       */
2146      abstract protected function get_typeid();
2147      /**
2148       * This method should return the string that describes the pagetype
2149       * @abstract
2150       * @return string
2151       */
2152      abstract protected function get_typestring();
2153  
2154      /**
2155       * This method gets called to display the page to the user taking the lesson
2156       * @abstract
2157       * @param object $renderer
2158       * @param object $attempt
2159       * @return string
2160       */
2161      abstract public function display($renderer, $attempt);
2162  
2163      /**
2164       * Creates a new lesson_page within the database and returns the correct pagetype
2165       * object to use to interact with the new lesson
2166       *
2167       * @final
2168       * @static
2169       * @param object $properties
2170       * @param lesson $lesson
2171       * @return lesson_page Specialised object that extends lesson_page
2172       */
2173      final public static function create($properties, lesson $lesson, $context, $maxbytes) {
2174          global $DB;
2175          $newpage = new stdClass;
2176          $newpage->title = $properties->title;
2177          $newpage->contents = $properties->contents_editor['text'];
2178          $newpage->contentsformat = $properties->contents_editor['format'];
2179          $newpage->lessonid = $lesson->id;
2180          $newpage->timecreated = time();
2181          $newpage->qtype = $properties->qtype;
2182          $newpage->qoption = (isset($properties->qoption))?1:0;
2183          $newpage->layout = (isset($properties->layout))?1:0;
2184          $newpage->display = (isset($properties->display))?1:0;
2185          $newpage->prevpageid = 0; // this is a first page
2186          $newpage->nextpageid = 0; // this is the only page
2187  
2188          if ($properties->pageid) {
2189              $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid');
2190              if (!$prevpage) {
2191                  print_error('cannotfindpages', 'lesson');
2192              }
2193              $newpage->prevpageid = $prevpage->id;
2194              $newpage->nextpageid = $prevpage->nextpageid;
2195          } else {
2196              $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id');
2197              if ($nextpage) {
2198                  // This is the first page, there are existing pages put this at the start
2199                  $newpage->nextpageid = $nextpage->id;
2200              }
2201          }
2202  
2203          $newpage->id = $DB->insert_record("lesson_pages", $newpage);
2204  
2205          $editor = new stdClass;
2206          $editor->id = $newpage->id;
2207          $editor->contents_editor = $properties->contents_editor;
2208          $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $editor->id);
2209          $DB->update_record("lesson_pages", $editor);
2210  
2211          if ($newpage->prevpageid > 0) {
2212              $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid));
2213          }
2214          if ($newpage->nextpageid > 0) {
2215              $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid));
2216          }
2217  
2218          $page = lesson_page::load($newpage, $lesson);
2219          $page->create_answers($properties);
2220  
2221          // Trigger an event: page created.
2222          $eventparams = array(
2223              'context' => $context,
2224              'objectid' => $newpage->id,
2225              'other' => array(
2226                  'pagetype' => $page->get_typestring()
2227                  )
2228              );
2229          $event = \mod_lesson\event\page_created::create($eventparams);
2230          $snapshot = clone($newpage);
2231          $snapshot->timemodified = 0;
2232          $event->add_record_snapshot('lesson_pages', $snapshot);
2233          $event->trigger();
2234  
2235          $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess');
2236  
2237          return $page;
2238      }
2239  
2240      /**
2241       * This method loads a page object from the database and returns it as a
2242       * specialised object that extends lesson_page
2243       *
2244       * @final
2245       * @static
2246       * @param int $id
2247       * @param lesson $lesson
2248       * @return lesson_page Specialised lesson_page object
2249       */
2250      final public static function load($id, lesson $lesson) {
2251          global $DB;
2252  
2253          if (is_object($id) && !empty($id->qtype)) {
2254              $page = $id;
2255          } else {
2256              $page = $DB->get_record("lesson_pages", array("id" => $id));
2257              if (!$page) {
2258                  print_error('cannotfindpages', 'lesson');
2259              }
2260          }
2261          $manager = lesson_page_type_manager::get($lesson);
2262  
2263          $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype);
2264          if (!class_exists($class)) {
2265              $class = 'lesson_page';
2266          }
2267  
2268          return new $class($page, $lesson);
2269      }
2270  
2271      /**
2272       * Deletes a lesson_page from the database as well as any associated records.
2273       * @final
2274       * @return bool
2275       */
2276      final public function delete() {
2277          global $DB;
2278  
2279          $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
2280          $context = context_module::instance($cm->id);
2281  
2282          // Delete files associated with attempts.
2283          $fs = get_file_storage();
2284          if ($attempts = $DB->get_records('lesson_attempts', array("pageid" => $this->properties->id))) {
2285              foreach ($attempts as $attempt) {
2286                  $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $attempt->id);
2287              }
2288          }
2289  
2290          // Then delete all the associated records...
2291          $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id));
2292  
2293          $DB->delete_records("lesson_branch", array("pageid" => $this->properties->id));
2294          // ...now delete the answers...
2295          $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id));
2296          // ..and the page itself
2297          $DB->delete_records("lesson_pages", array("id" => $this->properties->id));
2298  
2299          // Trigger an event: page deleted.
2300          $eventparams = array(
2301              'context' => $context,
2302              'objectid' => $this->properties->id,
2303              'other' => array(
2304                  'pagetype' => $this->get_typestring()
2305                  )
2306              );
2307          $event = \mod_lesson\event\page_deleted::create($eventparams);
2308          $event->add_record_snapshot('lesson_pages', $this->properties);
2309          $event->trigger();
2310  
2311          // Delete files associated with this page.
2312          $fs->delete_area_files($context->id, 'mod_lesson', 'page_contents', $this->properties->id);
2313          $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $this->properties->id);
2314          $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $this->properties->id);
2315  
2316          // repair the hole in the linkage
2317          if (!$this->properties->prevpageid && !$this->properties->nextpageid) {
2318              //This is the only page, no repair needed
2319          } elseif (!$this->properties->prevpageid) {
2320              // this is the first page...
2321              $page = $this->lesson->load_page($this->properties->nextpageid);
2322              $page->move(null, 0);
2323          } elseif (!$this->properties->nextpageid) {
2324              // this is the last page...
2325              $page = $this->lesson->load_page($this->properties->prevpageid);
2326              $page->move(0);
2327          } else {
2328              // page is in the middle...
2329              $prevpage = $this->lesson->load_page($this->properties->prevpageid);
2330              $nextpage = $this->lesson->load_page($this->properties->nextpageid);
2331  
2332              $prevpage->move($nextpage->id);
2333              $nextpage->move(null, $prevpage->id);
2334          }
2335          return true;
2336      }
2337  
2338      /**
2339       * Moves a page by updating its nextpageid and prevpageid values within
2340       * the database
2341       *
2342       * @final
2343       * @param int $nextpageid
2344       * @param int $prevpageid
2345       */
2346      final public function move($nextpageid=null, $prevpageid=null) {
2347          global $DB;
2348          if ($nextpageid === null) {
2349              $nextpageid = $this->properties->nextpageid;
2350          }
2351          if ($prevpageid === null) {
2352              $prevpageid = $this->properties->prevpageid;
2353          }
2354          $obj = new stdClass;
2355          $obj->id = $this->properties->id;
2356          $obj->prevpageid = $prevpageid;
2357          $obj->nextpageid = $nextpageid;
2358          $DB->update_record('lesson_pages', $obj);
2359      }
2360  
2361      /**
2362       * Returns the answers that are associated with this page in the database
2363       *
2364       * @final
2365       * @return array
2366       */
2367      final public function get_answers() {
2368          global $DB;
2369          if ($this->answers === null) {
2370              $this->answers = array();
2371              $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id');
2372              if (!$answers) {
2373                  // It is possible that a lesson upgraded from Moodle 1.9 still
2374                  // contains questions without any answers [MDL-25632].
2375                  // debugging(get_string('cannotfindanswer', 'lesson'));
2376                  return array();
2377              }
2378              foreach ($answers as $answer) {
2379                  $this->answers[count($this->answers)] = new lesson_page_answer($answer);
2380              }
2381          }
2382          return $this->answers;
2383      }
2384  
2385      /**
2386       * Returns the lesson this page is associated with
2387       * @final
2388       * @return lesson
2389       */
2390      final protected function get_lesson() {
2391          return $this->lesson;
2392      }
2393  
2394      /**
2395       * Returns the type of page this is. Not to be confused with page type
2396       * @final
2397       * @return int
2398       */
2399      final protected function get_type() {
2400          return $this->type;
2401      }
2402  
2403      /**
2404       * Records an attempt at this page
2405       *
2406       * @final
2407       * @global moodle_database $DB
2408       * @param stdClass $context
2409       * @return stdClass Returns the result of the attempt
2410       */
2411      final public function record_attempt($context) {
2412          global $DB, $USER, $OUTPUT, $PAGE;
2413  
2414          /**
2415           * This should be overridden by each page type to actually check the response
2416           * against what ever custom criteria they have defined
2417           */
2418          $result = $this->check_answer();
2419  
2420          $result->attemptsremaining  = 0;
2421          $result->maxattemptsreached = false;
2422  
2423          if ($result->noanswer) {
2424              $result->newpageid = $this->properties->id; // display same page again
2425              $result->feedback  = get_string('noanswer', 'lesson');
2426          } else {
2427              if (!has_capability('mod/lesson:manage', $context)) {
2428                  $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
2429  
2430                  // Get the number of attempts that have been made on this question for this student and retake,
2431                  $nattempts = $DB->count_records('lesson_attempts', array('lessonid' => $this->lesson->id,
2432                      'userid' => $USER->id, 'pageid' => $this->properties->id, 'retry' => $nretakes));
2433  
2434                  // Check if they have reached (or exceeded) the maximum number of attempts allowed.
2435                  if ($nattempts >= $this->lesson->maxattempts) {
2436                      $result->maxattemptsreached = true;
2437                      $result->feedback = get_string('maximumnumberofattemptsreached', 'lesson');
2438                      $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid);
2439                      return $result;
2440                  }
2441  
2442                  // record student's attempt
2443                  $attempt = new stdClass;
2444                  $attempt->lessonid = $this->lesson->id;
2445                  $attempt->pageid = $this->properties->id;
2446                  $attempt->userid = $USER->id;
2447                  $attempt->answerid = $result->answerid;
2448                  $attempt->retry = $nretakes;
2449                  $attempt->correct = $result->correctanswer;
2450                  if($result->userresponse !== null) {
2451                      $attempt->useranswer = $result->userresponse;
2452                  }
2453  
2454                  $attempt->timeseen = time();
2455                  // if allow modattempts, then update the old attempt record, otherwise, insert new answer record
2456                  $userisreviewing = false;
2457                  if (isset($USER->modattempts[$this->lesson->id])) {
2458                      $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high
2459                      $userisreviewing = true;
2460                  }
2461  
2462                  // Only insert a record if we are not reviewing the lesson.
2463                  if (!$userisreviewing) {
2464                      if ($this->lesson->retake || (!$this->lesson->retake && $nretakes == 0)) {
2465                          $attempt->id = $DB->insert_record("lesson_attempts", $attempt);
2466                          // Trigger an event: question answered.
2467                          $eventparams = array(
2468                              'context' => context_module::instance($PAGE->cm->id),
2469                              'objectid' => $this->properties->id,
2470                              'other' => array(
2471                                  'pagetype' => $this->get_typestring()
2472                                  )
2473                              );
2474                          $event = \mod_lesson\event\question_answered::create($eventparams);
2475                          $event->add_record_snapshot('lesson_attempts', $attempt);
2476                          $event->trigger();
2477  
2478                          // Increase the number of attempts made.
2479                          $nattempts++;
2480                      }
2481                  }
2482                  // "number of attempts remaining" message if $this->lesson->maxattempts > 1
2483                  // displaying of message(s) is at the end of page for more ergonomic display
2484                  if (!$result->correctanswer && ($result->newpageid == 0)) {
2485                      // retreive the number of attempts left counter for displaying at bottom of feedback page
2486                      if ($nattempts >= $this->lesson->maxattempts) {
2487                          if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2488                              $result->maxattemptsreached = true;
2489                          }
2490                          $result->newpageid = LESSON_NEXTPAGE;
2491                      } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2492                          $result->attemptsremaining = $this->lesson->maxattempts - $nattempts;
2493                      }
2494                  }
2495              }
2496              // TODO: merge this code with the jump code below.  Convert jumpto page into a proper page id
2497              if ($result->newpageid == 0) {
2498                  $result->newpageid = $this->properties->id;
2499              } elseif ($result->newpageid == LESSON_NEXTPAGE) {
2500                  $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid);
2501              }
2502  
2503              // Determine default feedback if necessary
2504              if (empty($result->response)) {
2505                  if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) {
2506                      // These conditions have been met:
2507                      //  1. The lesson manager has not supplied feedback to the student
2508                      //  2. Not displaying default feedback
2509                      //  3. The user did provide an answer
2510                      //  4. We are not reviewing with an incorrect answer (and not reviewing an essay question)
2511  
2512                      $result->nodefaultresponse = true;  // This will cause a redirect below
2513                  } else if ($result->isessayquestion) {
2514                      $result->response = get_string('defaultessayresponse', 'lesson');
2515                  } else if ($result->correctanswer) {
2516                      $result->response = get_string('thatsthecorrectanswer', 'lesson');
2517                  } else {
2518                      $result->response = get_string('thatsthewronganswer', 'lesson');
2519                  }
2520              }
2521  
2522              if ($result->response) {
2523                  if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) {
2524                      $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
2525                      $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id));
2526                      if ($qattempts == 1) {
2527                          $result->feedback = $OUTPUT->box(get_string("firstwrong", "lesson"), 'feedback');
2528                      } else {
2529                          $result->feedback = $OUTPUT->box(get_string("secondpluswrong", "lesson"), 'feedback');
2530                      }
2531                  } else {
2532                      $result->feedback = '';
2533                  }
2534                  $class = 'response';
2535                  if ($result->correctanswer) {
2536                      $class .= ' correct'; // CSS over-ride this if they exist (!important).
2537                  } else if (!$result->isessayquestion) {
2538                      $class .= ' incorrect'; // CSS over-ride this if they exist (!important).
2539                  }
2540                  $options = new stdClass;
2541                  $options->noclean = true;
2542                  $options->para = true;
2543                  $options->overflowdiv = true;
2544                  $options->context = $context;
2545  
2546                  $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options),
2547                          'generalbox boxaligncenter');
2548                  if (isset($result->studentanswerformat)) {
2549                      // This is the student's answer so it should be cleaned.
2550                      $studentanswer = format_text($result->studentanswer, $result->studentanswerformat,
2551                              array('context' => $context, 'para' => true));
2552                  } else {
2553                      $studentanswer = format_string($result->studentanswer);
2554                  }
2555                  $result->feedback .= '<div class="correctanswer generalbox"><em>'
2556                          . get_string("youranswer", "lesson").'</em> : ' . $studentanswer;
2557                  if (isset($result->responseformat)) {
2558                      $result->response = file_rewrite_pluginfile_urls($result->response, 'pluginfile.php', $context->id,
2559                              'mod_lesson', 'page_responses', $result->answerid);
2560                      $result->feedback .= $OUTPUT->box(format_text($result->response, $result->responseformat, $options)
2561                              , $class);
2562                  } else {
2563                      $result->feedback .= $OUTPUT->box($result->response, $class);
2564                  }
2565                  $result->feedback .= '</div>';
2566              }
2567          }
2568  
2569          return $result;
2570      }
2571  
2572      /**
2573       * Returns the string for a jump name
2574       *
2575       * @final
2576       * @param int $jumpto Jump code or page ID
2577       * @return string
2578       **/
2579      final protected function get_jump_name($jumpto) {
2580          global $DB;
2581          static $jumpnames = array();
2582  
2583          if (!array_key_exists($jumpto, $jumpnames)) {
2584              if ($jumpto == LESSON_THISPAGE) {
2585                  $jumptitle = get_string('thispage', 'lesson');
2586              } elseif ($jumpto == LESSON_NEXTPAGE) {
2587                  $jumptitle = get_string('nextpage', 'lesson');
2588              } elseif ($jumpto == LESSON_EOL) {
2589                  $jumptitle = get_string('endoflesson', 'lesson');
2590              } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
2591                  $jumptitle = get_string('unseenpageinbranch', 'lesson');
2592              } elseif ($jumpto == LESSON_PREVIOUSPAGE) {
2593                  $jumptitle = get_string('previouspage', 'lesson');
2594              } elseif ($jumpto == LESSON_RANDOMPAGE) {
2595                  $jumptitle = get_string('randompageinbranch', 'lesson');
2596              } elseif ($jumpto == LESSON_RANDOMBRANCH) {
2597                  $jumptitle = get_string('randombranch', 'lesson');
2598              } elseif ($jumpto == LESSON_CLUSTERJUMP) {
2599                  $jumptitle = get_string('clusterjump', 'lesson');
2600              } else {
2601                  if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) {
2602                      $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>';
2603                  }
2604              }
2605              $jumpnames[$jumpto] = format_string($jumptitle,true);
2606          }
2607  
2608          return $jumpnames[$jumpto];
2609      }
2610  
2611      /**
2612       * Constructor method
2613       * @param object $properties
2614       * @param lesson $lesson
2615       */
2616      public function __construct($properties, lesson $lesson) {
2617          parent::__construct($properties);
2618          $this->lesson = $lesson;
2619      }
2620  
2621      /**
2622       * Returns the score for the attempt
2623       * This may be overridden by page types that require manual grading
2624       * @param array $answers
2625       * @param object $attempt
2626       * @return int
2627       */
2628      public function earned_score($answers, $attempt) {
2629          return $answers[$attempt->answerid]->score;
2630      }
2631  
2632      /**
2633       * This is a callback method that can be override and gets called when ever a page
2634       * is viewed
2635       *
2636       * @param bool $canmanage True if the user has the manage cap
2637       * @return mixed
2638       */
2639      public function callback_on_view($canmanage) {
2640          return true;
2641      }
2642  
2643      /**
2644       * save editor answers files and update answer record
2645       *
2646       * @param object $context
2647       * @param int $maxbytes
2648       * @param object $answer
2649       * @param object $answereditor
2650       * @param object $responseeditor
2651       */
2652      public function save_answers_files($context, $maxbytes, &$answer, $answereditor = '', $responseeditor = '') {
2653          global $DB;
2654          if (isset($answereditor['itemid'])) {
2655              $answer->answer = file_save_draft_area_files($answereditor['itemid'],
2656                      $context->id, 'mod_lesson', 'page_answers', $answer->id,
2657                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
2658                      $answer->answer, null);
2659              $DB->set_field('lesson_answers', 'answer', $answer->answer, array('id' => $answer->id));
2660          }
2661          if (isset($responseeditor['itemid'])) {
2662              $answer->response = file_save_draft_area_files($responseeditor['itemid'],
2663                      $context->id, 'mod_lesson', 'page_responses', $answer->id,
2664                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
2665                      $answer->response, null);
2666              $DB->set_field('lesson_answers', 'response', $answer->response, array('id' => $answer->id));
2667          }
2668      }
2669  
2670      /**
2671       * Rewrite urls in response and optionality answer of a question answer
2672       *
2673       * @param object $answer
2674       * @param bool $rewriteanswer must rewrite answer
2675       * @return object answer with rewritten urls
2676       */
2677      public static function rewrite_answers_urls($answer, $rewriteanswer = true) {
2678          global $PAGE;
2679  
2680          $context = context_module::instance($PAGE->cm->id);
2681          if ($rewriteanswer) {
2682              $answer->answer = file_rewrite_pluginfile_urls($answer->answer, 'pluginfile.php', $context->id,
2683                      'mod_lesson', 'page_answers', $answer->id);
2684          }
2685          $answer->response = file_rewrite_pluginfile_urls($answer->response, 'pluginfile.php', $context->id,
2686                  'mod_lesson', 'page_responses', $answer->id);
2687  
2688          return $answer;
2689      }
2690  
2691      /**
2692       * Updates a lesson page and its answers within the database
2693       *
2694       * @param object $properties
2695       * @return bool
2696       */
2697      public function update($properties, $context = null, $maxbytes = null) {
2698          global $DB, $PAGE;
2699          $answers  = $this->get_answers();
2700          $properties->id = $this->properties->id;
2701          $properties->lessonid = $this->lesson->id;
2702          if (empty($properties->qoption)) {
2703              $properties->qoption = '0';
2704          }
2705          if (empty($context)) {
2706              $context = $PAGE->context;
2707          }
2708          if ($maxbytes === null) {
2709              $maxbytes = get_user_max_upload_file_size($context);
2710          }
2711          $properties->timemodified = time();
2712          $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $properties->id);
2713          $DB->update_record("lesson_pages", $properties);
2714  
2715          // Trigger an event: page updated.
2716          \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger();
2717  
2718          if ($this->type == self::TYPE_STRUCTURE && $this->get_typeid() != LESSON_PAGE_BRANCHTABLE) {
2719              // These page types have only one answer to save the jump and score.
2720              if (count($answers) > 1) {
2721                  $answer = array_shift($answers);
2722                  foreach ($answers as $a) {
2723                      $DB->delete_record('lesson_answers', array('id' => $a->id));
2724                  }
2725              } else if (count($answers) == 1) {
2726                  $answer = array_shift($answers);
2727              } else {
2728                  $answer = new stdClass;
2729                  $answer->lessonid = $properties->lessonid;
2730                  $answer->pageid = $properties->id;
2731                  $answer->timecreated = time();
2732              }
2733  
2734              $answer->timemodified = time();
2735              if (isset($properties->jumpto[0])) {
2736                  $answer->jumpto = $properties->jumpto[0];
2737              }
2738              if (isset($properties->score[0])) {
2739                  $answer->score = $properties->score[0];
2740              }
2741              if (!empty($answer->id)) {
2742                  $DB->update_record("lesson_answers", $answer->properties());
2743              } else {
2744                  $DB->insert_record("lesson_answers", $answer);
2745              }
2746          } else {
2747              for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2748                  if (!array_key_exists($i, $this->answers)) {
2749                      $this->answers[$i] = new stdClass;
2750                      $this->answers[$i]->lessonid = $this->lesson->id;
2751                      $this->answers[$i]->pageid = $this->id;
2752                      $this->answers[$i]->timecreated = $this->timecreated;
2753                  }
2754  
2755                  if (isset($properties->answer_editor[$i])) {
2756                      if (is_array($properties->answer_editor[$i])) {
2757                          // Multichoice and true/false pages have an HTML editor.
2758                          $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
2759                          $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
2760                      } else {
2761                          // Branch tables, shortanswer and mumerical pages have only a text field.
2762                          $this->answers[$i]->answer = $properties->answer_editor[$i];
2763                          $this->answers[$i]->answerformat = FORMAT_MOODLE;
2764                      }
2765                  }
2766  
2767                  if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
2768                      $this->answers[$i]->response = $properties->response_editor[$i]['text'];
2769                      $this->answers[$i]->responseformat = $properties->response_editor[$i]['format'];
2770                  }
2771  
2772                  if (isset($this->answers[$i]->answer) && $this->answers[$i]->answer != '') {
2773                      if (isset($properties->jumpto[$i])) {
2774                          $this->answers[$i]->jumpto = $properties->jumpto[$i];
2775                      }
2776                      if ($this->lesson->custom && isset($properties->score[$i])) {
2777                          $this->answers[$i]->score = $properties->score[$i];
2778                      }
2779                      if (!isset($this->answers[$i]->id)) {
2780                          $this->answers[$i]->id = $DB->insert_record("lesson_answers", $this->answers[$i]);
2781                      } else {
2782                          $DB->update_record("lesson_answers", $this->answers[$i]->properties());
2783                      }
2784  
2785                      // Save files in answers and responses.
2786                      if (isset($properties->response_editor[$i])) {
2787                          $this->save_answers_files($context, $maxbytes, $this->answers[$i],
2788                                  $properties->answer_editor[$i], $properties->response_editor[$i]);
2789                      } else {
2790                          $this->save_answers_files($context, $maxbytes, $this->answers[$i],
2791                                  $properties->answer_editor[$i]);
2792                      }
2793  
2794                  } else if (isset($this->answers[$i]->id)) {
2795                      $DB->delete_records('lesson_answers', array('id' => $this->answers[$i]->id));
2796                      unset($this->answers[$i]);
2797                  }
2798              }
2799          }
2800          return true;
2801      }
2802  
2803      /**
2804       * Can be set to true if the page requires a static link to create a new instance
2805       * instead of simply being included in the dropdown
2806       * @param int $previd
2807       * @return bool
2808       */
2809      public function add_page_link($previd) {
2810          return false;
2811      }
2812  
2813      /**
2814       * Returns true if a page has been viewed before
2815       *
2816       * @param array|int $param Either an array of pages that have been seen or the
2817       *                   number of retakes a user has had
2818       * @return bool
2819       */
2820      public function is_unseen($param) {
2821          global $USER, $DB;
2822          if (is_array($param)) {
2823              $seenpages = $param;
2824              return (!array_key_exists($this->properties->id, $seenpages));
2825          } else {
2826              $nretakes = $param;
2827              if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
2828                  return true;
2829              }
2830          }
2831          return false;
2832      }
2833  
2834      /**
2835       * Checks to see if a page has been answered previously
2836       * @param int $nretakes
2837       * @return bool
2838       */
2839      public function is_unanswered($nretakes) {
2840          global $DB, $USER;
2841          if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
2842              return true;
2843          }
2844          return false;
2845      }
2846  
2847      /**
2848       * Creates answers within the database for this lesson_page. Usually only ever
2849       * called when creating a new page instance
2850       * @param object $properties
2851       * @return array
2852       */
2853      public function create_answers($properties) {
2854          global $DB, $PAGE;
2855          // now add the answers
2856          $newanswer = new stdClass;
2857          $newanswer->lessonid = $this->lesson->id;
2858          $newanswer->pageid = $this->properties->id;
2859          $newanswer->timecreated = $this->properties->timecreated;
2860  
2861          $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
2862          $context = context_module::instance($cm->id);
2863  
2864          $answers = array();
2865  
2866          for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2867              $answer = clone($newanswer);
2868  
2869              if (!empty($properties->answer_editor[$i])) {
2870                  if (is_array($properties->answer_editor[$i])) {
2871                      // Multichoice and true/false pages have an HTML editor.
2872                      $answer->answer = $properties->answer_editor[$i]['text'];
2873                      $answer->answerformat = $properties->answer_editor[$i]['format'];
2874                  } else {
2875                      // Branch tables, shortanswer and mumerical pages have only a text field.
2876                      $answer->answer = $properties->answer_editor[$i];
2877                      $answer->answerformat = FORMAT_MOODLE;
2878                  }
2879              }
2880              if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
2881                  $answer->response = $properties->response_editor[$i]['text'];
2882                  $answer->responseformat = $properties->response_editor[$i]['format'];
2883              }
2884  
2885              if (isset($answer->answer) && $answer->answer != '') {
2886                  if (isset($properties->jumpto[$i])) {
2887                      $answer->jumpto = $properties->jumpto[$i];
2888                  }
2889                  if ($this->lesson->custom && isset($properties->score[$i])) {
2890                      $answer->score = $properties->score[$i];
2891                  }
2892                  $answer->id = $DB->insert_record("lesson_answers", $answer);
2893                  if (isset($properties->response_editor[$i])) {
2894                      $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
2895                              $properties->answer_editor[$i], $properties->response_editor[$i]);
2896                  } else {
2897                      $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
2898                              $properties->answer_editor[$i]);
2899                  }
2900                  $answers[$answer->id] = new lesson_page_answer($answer);
2901              } else {
2902                  break;
2903              }
2904          }
2905  
2906          $this->answers = $answers;
2907          return $answers;
2908      }
2909  
2910      /**
2911       * This method MUST be overridden by all question page types, or page types that
2912       * wish to score a page.
2913       *
2914       * The structure of result should always be the same so it is a good idea when
2915       * overriding this method on a page type to call
2916       * <code>
2917       * $result = parent::check_answer();
2918       * </code>
2919       * before modifying it as required.
2920       *
2921       * @return stdClass
2922       */
2923      public function check_answer() {
2924          $result = new stdClass;
2925          $result->answerid        = 0;
2926          $result->noanswer        = false;
2927          $result->correctanswer   = false;
2928          $result->isessayquestion = false;   // use this to turn off review button on essay questions
2929          $result->response        = '';
2930          $result->newpageid       = 0;       // stay on the page
2931          $result->studentanswer   = '';      // use this to store student's answer(s) in order to display it on feedback page
2932          $result->userresponse    = null;
2933          $result->feedback        = '';
2934          $result->nodefaultresponse  = false; // Flag for redirecting when default feedback is turned off
2935          return $result;
2936      }
2937  
2938      /**
2939       * True if the page uses a custom option
2940       *
2941       * Should be override and set to true if the page uses a custom option.
2942       *
2943       * @return bool
2944       */
2945      public function has_option() {
2946          return false;
2947      }
2948  
2949      /**
2950       * Returns the maximum number of answers for this page given the maximum number
2951       * of answers permitted by the lesson.
2952       *
2953       * @param int $default
2954       * @return int
2955       */
2956      public function max_answers($default) {
2957          return $default;
2958      }
2959  
2960      /**
2961       * Returns the properties of this lesson page as an object
2962       * @return stdClass;
2963       */
2964      public function properties() {
2965          $properties = clone($this->properties);
2966          if ($this->answers === null) {
2967              $this->get_answers();
2968          }
2969          if (count($this->answers)>0) {
2970              $count = 0;
2971              $qtype = $properties->qtype;
2972              foreach ($this->answers as $answer) {
2973                  $properties->{'answer_editor['.$count.']'} = array('text' => $answer->answer, 'format' => $answer->answerformat);
2974                  if ($qtype != LESSON_PAGE_MATCHING) {
2975                      $properties->{'response_editor['.$count.']'} = array('text' => $answer->response, 'format' => $answer->responseformat);
2976                  } else {
2977                      $properties->{'response_editor['.$count.']'} = $answer->response;
2978                  }
2979                  $properties->{'jumpto['.$count.']'} = $answer->jumpto;
2980                  $properties->{'score['.$count.']'} = $answer->score;
2981                  $count++;
2982              }
2983          }
2984          return $properties;
2985      }
2986  
2987      /**
2988       * Returns an array of options to display when choosing the jumpto for a page/answer
2989       * @static
2990       * @param int $pageid
2991       * @param lesson $lesson
2992       * @return array
2993       */
2994      public static function get_jumptooptions($pageid, lesson $lesson) {
2995          global $DB;
2996          $jump = array();
2997          $jump[0] = get_string("thispage", "lesson");
2998          $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson");
2999          $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson");
3000          $jump[LESSON_EOL] = get_string("endoflesson", "lesson");
3001  
3002          if ($pageid == 0) {
3003              return $jump;
3004          }
3005  
3006          $pages = $lesson->load_all_pages();
3007          if ($pages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))) {
3008              $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson");
3009              $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson");
3010          }
3011          if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) {
3012              $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson");
3013          }
3014          if (!optional_param('firstpage', 0, PARAM_INT)) {
3015              $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0));
3016              while (true) {
3017                  if ($apageid) {
3018                      $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid));
3019                      $jump[$apageid] = strip_tags(format_string($title,true));
3020                      $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid));
3021                  } else {
3022                      // last page reached
3023                      break;
3024                  }
3025              }
3026          }
3027          return $jump;
3028      }
3029      /**
3030       * Returns the contents field for the page properly formatted and with plugin
3031       * file url's converted
3032       * @return string
3033       */
3034      public function get_contents() {
3035          global $PAGE;
3036          if (!empty($this->properties->contents)) {
3037              if (!isset($this->properties->contentsformat)) {
3038                  $this->properties->contentsformat = FORMAT_HTML;
3039              }
3040              $context = context_module::instance($PAGE->cm->id);
3041              $contents = file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'mod_lesson',
3042                                                       'page_contents', $this->properties->id);  // Must do this BEFORE format_text()!
3043              return format_text($contents, $this->properties->contentsformat,
3044                                 array('context' => $context, 'noclean' => true,
3045                                       'overflowdiv' => true));  // Page edit is marked with XSS, we want all content here.
3046          } else {
3047              return '';
3048          }
3049      }
3050  
3051      /**
3052       * Set to true if this page should display in the menu block
3053       * @return bool
3054       */
3055      protected function get_displayinmenublock() {
3056          return false;
3057      }
3058  
3059      /**
3060       * Get the string that describes the options of this page type
3061       * @return string
3062       */
3063      public function option_description_string() {
3064          return '';
3065      }
3066  
3067      /**
3068       * Updates a table with the answers for this page
3069       * @param html_table $table
3070       * @return html_table
3071       */
3072      public function display_answers(html_table $table) {
3073          $answers = $this->get_answers();
3074          $i = 1;
3075          foreach ($answers as $answer) {
3076              $cells = array();
3077              $cells[] = "<span class=\"label\">".get_string("jump", "lesson")." $i<span>: ";
3078              $cells[] = $this->get_jump_name($answer->jumpto);
3079              $table->data[] = new html_table_row($cells);
3080              if ($i === 1){
3081                  $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
3082              }
3083              $i++;
3084          }
3085          return $table;
3086      }
3087  
3088      /**
3089       * Determines if this page should be grayed out on the management/report screens
3090       * @return int 0 or 1
3091       */
3092      protected function get_grayout() {
3093          return 0;
3094      }
3095  
3096      /**
3097       * Adds stats for this page to the &pagestats object. This should be defined
3098       * for all page types that grade
3099       * @param array $pagestats
3100       * @param int $tries
3101       * @return bool
3102       */
3103      public function stats(array &$pagestats, $tries) {
3104          return true;
3105      }
3106  
3107      /**
3108       * Formats the answers of this page for a report
3109       *
3110       * @param object $answerpage
3111       * @param object $answerdata
3112       * @param object $useranswer
3113       * @param array $pagestats
3114       * @param int $i Count of first level answers
3115       * @param int $n Count of second level answers
3116       * @return object The answer page for this
3117       */
3118      public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
3119          $answers = $this->get_answers();
3120          $formattextdefoptions = new stdClass;
3121          $formattextdefoptions->para = false;  //I'll use it widely in this page
3122          foreach ($answers as $answer) {
3123              $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto));
3124              $answerdata->answers[] = array($data, "");
3125              $answerpage->answerdata = $answerdata;
3126          }
3127          return $answerpage;
3128      }
3129  
3130      /**
3131       * Gets an array of the jumps used by the answers of this page
3132       *
3133       * @return array
3134       */
3135      public function get_jumps() {
3136          global $DB;
3137          $jumps = array();
3138          $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id);
3139          if ($answers = $this->get_answers()) {
3140              foreach ($answers as $answer) {
3141                  $jumps[] = $this->get_jump_name($answer->jumpto);
3142              }
3143          } else {
3144              $jumps[] = $this->get_jump_name($this->properties->nextpageid);
3145          }
3146          return $jumps;
3147      }
3148      /**
3149       * Informs whether this page type require manual grading or not
3150       * @return bool
3151       */
3152      public function requires_manual_grading() {
3153          return false;
3154      }
3155  
3156      /**
3157       * A callback method that allows a page to override the next page a user will
3158       * see during when this page is being completed.
3159       * @return false|int
3160       */
3161      public function override_next_page() {
3162          return false;
3163      }
3164  
3165      /**
3166       * This method is used to determine if this page is a valid page
3167       *
3168       * @param array $validpages
3169       * @param array $pageviews
3170       * @return int The next page id to check
3171       */
3172      public function valid_page_and_view(&$validpages, &$pageviews) {
3173          $validpages[$this->properties->id] = 1;
3174          return $this->properties->nextpageid;
3175      }
3176  }
3177  
3178  
3179  
3180  /**
3181   * Class used to represent an answer to a page
3182   *
3183   * @property int $id The ID of this answer in the database
3184   * @property int $lessonid The ID of the lesson this answer belongs to
3185   * @property int $pageid The ID of the page this answer belongs to
3186   * @property int $jumpto Identifies where the user goes upon completing a page with this answer
3187   * @property int $grade The grade this answer is worth
3188   * @property int $score The score this answer will give
3189   * @property int $flags Used to store options for the answer
3190   * @property int $timecreated A timestamp of when the answer was created
3191   * @property int $timemodified A timestamp of when the answer was modified
3192   * @property string $answer The answer itself
3193   * @property string $response The response the user sees if selecting this answer
3194   *
3195   * @copyright  2009 Sam Hemelryk
3196   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3197   */
3198  class lesson_page_answer extends lesson_base {
3199  
3200      /**
3201       * Loads an page answer from the DB
3202       *
3203       * @param int $id
3204       * @return lesson_page_answer
3205       */
3206      public static function load($id) {
3207          global $DB;
3208          $answer = $DB->get_record("lesson_answers", array("id" => $id));
3209          return new lesson_page_answer($answer);
3210      }
3211  
3212      /**
3213       * Given an object of properties and a page created answer(s) and saves them
3214       * in the database.
3215       *
3216       * @param stdClass $properties
3217       * @param lesson_page $page
3218       * @return array
3219       */
3220      public static function create($properties, lesson_page $page) {
3221          return $page->create_answers($properties);
3222      }
3223  
3224  }
3225  
3226  /**
3227   * A management class for page types
3228   *
3229   * This class is responsible for managing the different pages. A manager object can
3230   * be retrieved by calling the following line of code:
3231   * <code>
3232   * $manager  = lesson_page_type_manager::get($lesson);
3233   * </code>
3234   * The first time the page type manager is retrieved the it includes all of the
3235   * different page types located in mod/lesson/pagetypes.
3236   *
3237   * @copyright  2009 Sam Hemelryk
3238   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3239   */
3240  class lesson_page_type_manager {
3241  
3242      /**
3243       * An array of different page type classes
3244       * @var array
3245       */
3246      protected $types = array();
3247  
3248      /**
3249       * Retrieves the lesson page type manager object
3250       *
3251       * If the object hasn't yet been created it is created here.
3252       *
3253       * @staticvar lesson_page_type_manager $pagetypemanager
3254       * @param lesson $lesson
3255       * @return lesson_page_type_manager
3256       */
3257      public static function get(lesson $lesson) {
3258          static $pagetypemanager;
3259          if (!($pagetypemanager instanceof lesson_page_type_manager)) {
3260              $pagetypemanager = new lesson_page_type_manager();
3261              $pagetypemanager->load_lesson_types($lesson);
3262          }
3263          return $pagetypemanager;
3264      }
3265  
3266      /**
3267       * Finds and loads all lesson page types in mod/lesson/pagetypes
3268       *
3269       * @param lesson $lesson
3270       */
3271      public function load_lesson_types(lesson $lesson) {
3272          global $CFG;
3273          $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/';
3274          $dir = dir($basedir);
3275          while (false !== ($entry = $dir->read())) {
3276              if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) {
3277                  continue;
3278              }
3279              require_once($basedir.$entry);
3280              $class = 'lesson_page_type_'.strtok($entry,'.');
3281              if (class_exists($class)) {
3282                  $pagetype = new $class(new stdClass, $lesson);
3283                  $this->types[$pagetype->typeid] = $pagetype;
3284              }
3285          }
3286  
3287      }
3288  
3289      /**
3290       * Returns an array of strings to describe the loaded page types
3291       *
3292       * @param int $type Can be used to return JUST the string for the requested type
3293       * @return array
3294       */
3295      public function get_page_type_strings($type=null, $special=true) {
3296          $types = array();
3297          foreach ($this->types as $pagetype) {
3298              if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) {
3299                  $types[$pagetype->typeid] = $pagetype->typestring;
3300              }
3301          }
3302          return $types;
3303      }
3304  
3305      /**
3306       * Returns the basic string used to identify a page type provided with an id
3307       *
3308       * This string can be used to instantiate or identify the page type class.
3309       * If the page type id is unknown then 'unknown' is returned
3310       *
3311       * @param int $id
3312       * @return string
3313       */
3314      public function get_page_type_idstring($id) {
3315          foreach ($this->types as $pagetype) {
3316              if ((int)$pagetype->typeid === (int)$id) {
3317                  return $pagetype->idstring;
3318              }
3319          }
3320          return 'unknown';
3321      }
3322  
3323      /**
3324       * Loads a page for the provided lesson given it's id
3325       *
3326       * This function loads a page from the lesson when given both the lesson it belongs
3327       * to as well as the page's id.
3328       * If the page doesn't exist an error is thrown
3329       *
3330       * @param int $pageid The id of the page to load
3331       * @param lesson $lesson The lesson the page belongs to
3332       * @return lesson_page A class that extends lesson_page
3333       */
3334      public function load_page($pageid, lesson $lesson) {
3335          global $DB;
3336          if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) {
3337              print_error('cannotfindpages', 'lesson');
3338          }
3339          $pagetype = get_class($this->types[$page->qtype]);
3340          $page = new $pagetype($page, $lesson);
3341          return $page;
3342      }
3343  
3344      /**
3345       * This function detects errors in the ordering between 2 pages and updates the page records.
3346       *
3347       * @param stdClass $page1 Either the first of 2 pages or null if the $page2 param is the first in the list.
3348       * @param stdClass $page1 Either the second of 2 pages or null if the $page1 param is the last in the list.
3349       */
3350      protected function check_page_order($page1, $page2) {
3351          global $DB;
3352          if (empty($page1)) {
3353              if ($page2->prevpageid != 0) {
3354                  debugging("***prevpageid of page " . $page2->id . " set to 0***");
3355                  $page2->prevpageid = 0;
3356                  $DB->set_field("lesson_pages", "prevpageid", 0, array("id" => $page2->id));
3357              }
3358          } else if (empty($page2)) {
3359              if ($page1->nextpageid != 0) {
3360                  debugging("***nextpageid of page " . $page1->id . " set to 0***");
3361                  $page1->nextpageid = 0;
3362                  $DB->set_field("lesson_pages", "nextpageid", 0, array("id" => $page1->id));
3363              }
3364          } else {
3365              if ($page1->nextpageid != $page2->id) {
3366                  debugging("***nextpageid of page " . $page1->id . " set to " . $page2->id . "***");
3367                  $page1->nextpageid = $page2->id;
3368                  $DB->set_field("lesson_pages", "nextpageid", $page2->id, array("id" => $page1->id));
3369              }
3370              if ($page2->prevpageid != $page1->id) {
3371                  debugging("***prevpageid of page " . $page2->id . " set to " . $page1->id . "***");
3372                  $page2->prevpageid = $page1->id;
3373                  $DB->set_field("lesson_pages", "prevpageid", $page1->id, array("id" => $page2->id));
3374              }
3375          }
3376      }
3377  
3378      /**
3379       * This function loads ALL pages that belong to the lesson.
3380       *
3381       * @param lesson $lesson
3382       * @return array An array of lesson_page_type_*
3383       */
3384      public function load_all_pages(lesson $lesson) {
3385          global $DB;
3386          if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) {
3387              return array(); // Records returned empty.
3388          }
3389          foreach ($pages as $key=>$page) {
3390              $pagetype = get_class($this->types[$page->qtype]);
3391              $pages[$key] = new $pagetype($page, $lesson);
3392          }
3393  
3394          $orderedpages = array();
3395          $lastpageid = 0;
3396          $morepages = true;
3397          while ($morepages) {
3398              $morepages = false;
3399              foreach ($pages as $page) {
3400                  if ((int)$page->prevpageid === (int)$lastpageid) {
3401                      // Check for errors in page ordering and fix them on the fly.
3402                      $prevpage = null;
3403                      if ($lastpageid !== 0) {
3404                          $prevpage = $orderedpages[$lastpageid];
3405                      }
3406                      $this->check_page_order($prevpage, $page);
3407                      $morepages = true;
3408                      $orderedpages[$page->id] = $page;
3409                      unset($pages[$page->id]);
3410                      $lastpageid = $page->id;
3411                      if ((int)$page->nextpageid===0) {
3412                          break 2;
3413                      } else {
3414                          break 1;
3415                      }
3416                  }
3417              }
3418          }
3419  
3420          // Add remaining pages and fix the nextpageid links for each page.
3421          foreach ($pages as $page) {
3422              // Check for errors in page ordering and fix them on the fly.
3423              $prevpage = null;
3424              if ($lastpageid !== 0) {
3425                  $prevpage = $orderedpages[$lastpageid];
3426              }
3427              $this->check_page_order($prevpage, $page);
3428              $orderedpages[$page->id] = $page;
3429              unset($pages[$page->id]);
3430              $lastpageid = $page->id;
3431          }
3432  
3433          if ($lastpageid !== 0) {
3434              $this->check_page_order($orderedpages[$lastpageid], null);
3435          }
3436  
3437          return $orderedpages;
3438      }
3439  
3440      /**
3441       * Fetches an mform that can be used to create/edit an page
3442       *
3443       * @param int $type The id for the page type
3444       * @param array $arguments Any arguments to pass to the mform
3445       * @return lesson_add_page_form_base
3446       */
3447      public function get_page_form($type, $arguments) {
3448          $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type);
3449          if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') {
3450              debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER);
3451              $class = 'lesson_add_page_form_selection';
3452          } else if ($class === 'lesson_add_page_form_unknown') {
3453              $class = 'lesson_add_page_form_selection';
3454          }
3455          return new $class(null, $arguments);
3456      }
3457  
3458      /**
3459       * Returns an array of links to use as add page links
3460       * @param int $previd The id of the previous page
3461       * @return array
3462       */
3463      public function get_add_page_type_links($previd) {
3464          global $OUTPUT;
3465  
3466          $links = array();
3467  
3468          foreach ($this->types as $key=>$type) {
3469              if ($link = $type->add_page_link($previd)) {
3470                  $links[$key] = $link;
3471              }
3472          }
3473  
3474          return $links;
3475      }
3476  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1